const startTimeSettings = { pomodoro: 25 * 60 * 1000, shortBreak: 5 * 60 * 1000, longBreak: 15 * 60 * 1000 }; let currentMode = 'pomodoro'; let remainingTimeMs = startTimeSettings[currentMode]; let countdownTimer = null; let isRunning = false; let targetDate = null; const startBtn = document.getElementById('startBtn'); const modeBtns = document.querySelectorAll('.mode-btn'); function getTimeSegmentElements(segmentElement) { const segmentDisplay = segmentElement.querySelector('.segment-display'); const segmentDisplayTop = segmentDisplay.querySelector('.segment-display__top'); const segmentDisplayBottom = segmentDisplay.querySelector('.segment-display__bottom'); const segmentOverlay = segmentDisplay.querySelector('.segment-overlay'); const segmentOverlayTop = segmentOverlay.querySelector('.segment-overlay__top'); const segmentOverlayBottom = segmentOverlay.querySelector('.segment-overlay__bottom'); return { segmentDisplayTop, segmentDisplayBottom, segmentOverlay, segmentOverlayTop, segmentOverlayBottom }; } function updateSegmentValues(displayElement, overlayElement, value) { displayElement.textContent = value; overlayElement.textContent = value; } function updateTimeSegment(segmentElement, timeValue) { const segmentElements = getTimeSegmentElements(segmentElement); if (parseInt(segmentElements.segmentDisplayTop.textContent, 10) === timeValue) { return; } segmentElements.segmentOverlay.classList.add('flip'); updateSegmentValues( segmentElements.segmentDisplayTop, segmentElements.segmentOverlayBottom, timeValue ); function finishAnimation() { segmentElements.segmentOverlay.classList.remove('flip'); updateSegmentValues( segmentElements.segmentDisplayBottom, segmentElements.segmentOverlayTop, timeValue ); this.removeEventListener('animationend', finishAnimation); } segmentElements.segmentOverlay.addEventListener('animationend', finishAnimation); } function updateTimeSection(sectionID, timeValue) { const sectionElement = document.getElementById(sectionID); if (!sectionElement) return; const timeGroup = sectionElement.querySelector('.time-group'); if (!timeGroup) return; let timeStr = timeValue.toString(); if (timeStr.length < 2) timeStr = timeStr.padStart(2, '0'); let timeSegments = timeGroup.querySelectorAll('.time-segment'); // Add more segments if needed while (timeSegments.length < timeStr.length) { const newSeg = timeSegments[0].cloneNode(true); const overlay = newSeg.querySelector('.segment-overlay'); if (overlay) overlay.classList.remove('flip'); const displays = newSeg.querySelectorAll('.segment-display__top, .segment-display__bottom, .segment-overlay__top, .segment-overlay__bottom'); displays.forEach(d => d.textContent = '0'); timeGroup.appendChild(newSeg); timeSegments = timeGroup.querySelectorAll('.time-segment'); } // Remove extra segments if not needed (but keep at least 2) while (timeSegments.length > Math.max(2, timeStr.length)) { timeGroup.removeChild(timeSegments[timeSegments.length - 1]); timeSegments = timeGroup.querySelectorAll('.time-segment'); } for (let i = 0; i < timeStr.length; i++) { updateTimeSegment(timeSegments[i], parseInt(timeStr[i], 10)); } } function getTimeRemaining(targetDateTime) { const nowTime = Date.now(); const complete = nowTime >= targetDateTime; if (complete) { return { complete, seconds: 0, minutes: 0, hours: 0 }; } const secondsRemaining = Math.max(0, Math.round((targetDateTime - nowTime) / 1000)); const minutes = Math.floor(secondsRemaining / 60); const seconds = secondsRemaining % 60; return { complete, seconds, minutes, hours: 0 }; } function calculateTimeBitsStatic(ms) { if (ms <= 0) return { complete: true, seconds: 0, minutes: 0, hours: 0 }; const secondsRemaining = Math.round(ms / 1000); const minutes = Math.floor(secondsRemaining / 60); const seconds = secondsRemaining % 60; return { complete: false, seconds, minutes, hours: 0 }; } function updateAllSegments() { let timeRemainingBits; if (!isRunning) { timeRemainingBits = calculateTimeBitsStatic(remainingTimeMs); } else { timeRemainingBits = getTimeRemaining(targetDate.getTime()); } updateTimeSection('seconds', timeRemainingBits.seconds); updateTimeSection('minutes', timeRemainingBits.minutes); updateTimeSection('hours', timeRemainingBits.hours); // Basic & Animation Time labels let timeStr = `${timeRemainingBits.minutes.toString().padStart(2, '0')}:${timeRemainingBits.seconds.toString().padStart(2, '0')}`; if (timeRemainingBits.hours > 0) { timeStr = `${timeRemainingBits.hours.toString().padStart(2, '0')}:` + timeStr; } const basicTimeLabel = document.getElementById('basicTimeLabel'); const animationTimeLabel = document.getElementById('animationTimeLabel'); // Basic mode digit-change animation if (basicTimeLabel) { basicTimeLabel._prevText = timeStr; basicTimeLabel.textContent = timeStr; } if (animationTimeLabel) animationTimeLabel.textContent = timeStr; // Progress calculation (shared by basic & animation) const maxMs = startTimeSettings[currentMode]; let currentMs = remainingTimeMs; if (isRunning && targetDate) { currentMs = Math.max(0, targetDate.getTime() - Date.now()); } const progress = 1 - (currentMs / maxMs); // Basic mode progress bar const basicProgressBar = document.getElementById('basicProgressBar'); if (basicProgressBar) { basicProgressBar.style.width = (progress * 100) + '%'; } // Animation Mode Ring const ring = document.getElementById('animationProgressRing'); if (ring) { const circumference = 2 * Math.PI * 150; ring.style.strokeDasharray = circumference; ring.style.strokeDashoffset = circumference * (1 - progress); } // Animation Mode Hamster orbit along circle const circleHamster = document.getElementById('circleHamster'); if (circleHamster) { // Keep hamster centered at top (or middle depending on CSS) // circleHamster.style.transform = `none`; // Pause/unpause hamster running animation if (!isRunning) { circleHamster.classList.add('paused'); } else { circleHamster.classList.remove('paused'); } } return timeRemainingBits.complete; } // Timer Controls function toggleTimer() { if (isRunning) { // Pause clearInterval(countdownTimer); isRunning = false; remainingTimeMs = targetDate.getTime() - Date.now(); if (remainingTimeMs < 0) remainingTimeMs = 0; startBtn.textContent = 'START'; setAnimalAnimationPaused(true); } else { // Start if (remainingTimeMs <= 0) return; targetDate = new Date(Date.now() + remainingTimeMs); isRunning = true; startBtn.textContent = 'PAUSE'; // Track Start Clicks if (typeof trackStartClick === 'function') { trackStartClick(); } setAnimalAnimationPaused(false); updateAllSegments(); countdownTimer = setInterval(() => { const isComplete = updateAllSegments(); const maxMs = startTimeSettings[currentMode]; const currentRemaining = Math.max(0, targetDate.getTime() - Date.now()); const p = Math.max(0, Math.min(100, ((maxMs - currentRemaining) / maxMs) * 100)); const pb = document.getElementById('trackerProgressBar'); if (pb) pb.style.width = p + '%'; if (isComplete) { clearInterval(countdownTimer); isRunning = false; remainingTimeMs = 0; startBtn.textContent = 'START'; setAnimalAnimationPaused(true); const pb = document.getElementById('trackerProgressBar'); if (pb) pb.style.width = '0%'; if (currentMode === 'pomodoro' && typeof finishPomodoroSession === 'function') { finishPomodoroSession(); } } }, 1000); } } function resetTimer() { if (countdownTimer) clearInterval(countdownTimer); isRunning = false; remainingTimeMs = startTimeSettings[currentMode]; targetDate = null; startBtn.textContent = 'START'; updateAllSegments(); setAnimalAnimationPaused(true); const pb = document.getElementById('trackerProgressBar'); if (pb) pb.style.width = '0%'; } startBtn.addEventListener('click', toggleTimer); const userResetBtn = document.getElementById('resetBtn'); if (userResetBtn) { userResetBtn.addEventListener('click', resetTimer); } // Display Mode Logic let currentDisplayMode = 'flip'; const displayModeBtn = document.getElementById('displayModeBtn'); const displayModeMenu = document.getElementById('displayModeMenu'); const displayModeItems = document.querySelectorAll('.display-mode-item'); const flipClockDisplay = document.getElementById('flipClockDisplay'); const basicClockDisplay = document.getElementById('basicClockDisplay'); const animationClockDisplay = document.getElementById('animationClockDisplay'); if (displayModeBtn) { displayModeBtn.addEventListener('click', (e) => { e.stopPropagation(); displayModeMenu.classList.toggle('hidden'); }); } document.addEventListener('click', (e) => { if (displayModeMenu && !displayModeMenu.classList.contains('hidden') && !e.target.closest('#displayModeBtn')) { displayModeMenu.classList.add('hidden'); } }); displayModeItems.forEach(item => { item.addEventListener('click', () => { const mode = item.getAttribute('data-display'); currentDisplayMode = mode; displayModeItems.forEach(i => i.classList.remove('active')); item.classList.add('active'); if (flipClockDisplay) flipClockDisplay.classList.add('hidden'); if (basicClockDisplay) basicClockDisplay.classList.add('hidden'); if (animationClockDisplay) animationClockDisplay.classList.add('hidden'); if (mode === 'flip' && flipClockDisplay) flipClockDisplay.classList.remove('hidden'); if (mode === 'basic' && basicClockDisplay) basicClockDisplay.classList.remove('hidden'); if (mode === 'animation') { if (animationClockDisplay) animationClockDisplay.classList.remove('hidden'); document.body.classList.add('animation-mode'); } else { document.body.classList.remove('animation-mode'); } displayModeMenu.classList.add('hidden'); // Save to localStorage try { localStorage.setItem('pomodoroDisplayMode', mode); } catch (e) { } }); }); // Mode Selection modeBtns.forEach(btn => { btn.addEventListener('click', () => { // Styling modeBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Logic currentMode = btn.getAttribute('data-mode'); resetTimer(); }); }); // Settings & Toggles const settingsModal = document.getElementById('settingsModal'); const settingsToggle = document.getElementById('settingsToggle'); const closeSettings = document.getElementById('closeSettings'); const saveSettingsBtn = document.getElementById('saveSettingsBtn'); const showHoursToggle = document.getElementById('showHoursToggle'); if (settingsToggle) settingsToggle.addEventListener('click', () => settingsModal.classList.remove('hidden')); if (closeSettings) closeSettings.addEventListener('click', () => settingsModal.classList.add('hidden')); if (saveSettingsBtn) saveSettingsBtn.addEventListener('click', () => { // Update Times startTimeSettings.pomodoro = parseInt(document.getElementById('setPomodoro').value) * 60 * 1000; startTimeSettings.shortBreak = parseInt(document.getElementById('setShort').value) * 60 * 1000; startTimeSettings.longBreak = parseInt(document.getElementById('setLong').value) * 60 * 1000; // Update Colors & Fonts document.documentElement.style.setProperty('--bg-color', document.getElementById('bgColor').value); document.documentElement.style.setProperty('--theme-color', document.getElementById('themeColor').value); document.documentElement.style.setProperty('--segment-bg', document.getElementById('segmentBgColor').value); document.documentElement.style.setProperty('--font-family', document.getElementById('fontPicker').value); // Hours Visibility const hoursSection = document.getElementById('hours'); const hoursColon = document.getElementById('hoursColon'); if (showHoursToggle && showHoursToggle.checked) { hoursSection.classList.remove('hidden'); hoursColon.classList.remove('hidden'); } else { hoursSection.classList.add('hidden'); hoursColon.classList.add('hidden'); } settingsModal.classList.add('hidden'); resetTimer(); }); // Reset Settings to Defaults const resetSettingsBtn = document.getElementById('resetSettingsBtn'); if (resetSettingsBtn) resetSettingsBtn.addEventListener('click', () => { // Default values const defaults = { pomodoro: 25, shortBreak: 5, longBreak: 15, bgColor: '#121212', themeColor: '#ffffff', segmentBgColor: '#000000', fontFamily: 'Inter, sans-serif', trackerScale: 1 }; // Reset inputs document.getElementById('setPomodoro').value = defaults.pomodoro; document.getElementById('setShort').value = defaults.shortBreak; document.getElementById('setLong').value = defaults.longBreak; document.getElementById('bgColor').value = defaults.bgColor; document.getElementById('themeColor').value = defaults.themeColor; document.getElementById('segmentBgColor').value = defaults.segmentBgColor; document.getElementById('fontPicker').value = defaults.fontFamily; const trackerSlider = document.getElementById('trackerSizeSlider'); if (trackerSlider) trackerSlider.value = defaults.trackerScale; // Apply CSS variables document.documentElement.style.setProperty('--bg-color', defaults.bgColor); document.documentElement.style.setProperty('--theme-color', defaults.themeColor); document.documentElement.style.setProperty('--segment-bg', defaults.segmentBgColor); document.documentElement.style.setProperty('--font-family', defaults.fontFamily); document.documentElement.style.setProperty('--tracker-scale', defaults.trackerScale); // Reset times startTimeSettings.pomodoro = defaults.pomodoro * 60 * 1000; startTimeSettings.shortBreak = defaults.shortBreak * 60 * 1000; startTimeSettings.longBreak = defaults.longBreak * 60 * 1000; resetTimer(); }); // Spotify Widget const spotifyWidget = document.getElementById('spotifyWidget'); const spotifyToggle = document.getElementById('spotifyToggle'); const closeSpotify = document.getElementById('closeSpotify'); const loadSpotifyBtn = document.getElementById('loadSpotifyBtn'); const spotifyLink = document.getElementById('spotifyLink'); const spotifyIframe = document.getElementById('spotifyIframe'); if (spotifyToggle) spotifyToggle.addEventListener('click', () => spotifyWidget.classList.toggle('hidden')); if (closeSpotify) closeSpotify.addEventListener('click', () => spotifyWidget.classList.add('hidden')); if (loadSpotifyBtn) loadSpotifyBtn.addEventListener('click', () => { const rawUrl = spotifyLink.value.trim(); if (!rawUrl) return; let embedUrl = rawUrl; if (rawUrl.includes('open.spotify.com')) { // Strip ?si= and other params; build clean embed URL with dark theme const match = rawUrl.match(/open\.spotify\.com\/(embed\/)?(playlist|album|track|episode|show)\/([a-zA-Z0-9]+)/); if (match) { embedUrl = `https://open.spotify.com/embed/${match[2]}/${match[3]}?utm_source=generator&theme=0`; } } spotifyIframe.src = embedUrl; }); // Init updateAllSegments(); // Restore display mode from localStorage (function restoreDisplayMode() { try { const saved = localStorage.getItem('pomodoroDisplayMode'); if (saved && ['flip', 'basic', 'animation'].includes(saved)) { currentDisplayMode = saved; displayModeItems.forEach(i => { i.classList.toggle('active', i.getAttribute('data-display') === saved); }); if (flipClockDisplay) flipClockDisplay.classList.add('hidden'); if (basicClockDisplay) basicClockDisplay.classList.add('hidden'); if (animationClockDisplay) animationClockDisplay.classList.add('hidden'); if (saved === 'flip' && flipClockDisplay) flipClockDisplay.classList.remove('hidden'); if (saved === 'basic' && basicClockDisplay) basicClockDisplay.classList.remove('hidden'); if (saved === 'animation') { if (animationClockDisplay) animationClockDisplay.classList.remove('hidden'); document.body.classList.add('animation-mode'); } else { document.body.classList.remove('animation-mode'); } } } catch (e) { } })(); // Animal Switcher Logic function setAnimalAnimationPaused(isPaused) { const activeAnim = document.querySelector('.animal-animation:not(.hidden)'); if (activeAnim) { if (isPaused) { activeAnim.classList.add('paused'); } else { activeAnim.classList.remove('paused'); } } const circleHamster = document.getElementById('circleHamster'); if (circleHamster) { if (isPaused) { circleHamster.classList.add('paused'); } else { circleHamster.classList.remove('paused'); } } } const animals = [ { id: 'hamsterAnim', name: 'BigBike' }, { id: 'turtlesAnim', name: 'Heng and Dee' }, { id: 'dogAnim', name: 'Meenam' }, { id: 'capybaraAnim', name: 'FeiFei' } ]; let currentAnimalIndex = 0; const animalSwitcherBtn = document.getElementById('animalSwitcher'); const animalNameLabel = document.getElementById('animalName'); const animalMenu = document.getElementById('animalSelectionMenu'); const animalCards = document.querySelectorAll('.animal-card'); if (animalSwitcherBtn) { animalSwitcherBtn.addEventListener('click', (e) => { e.stopPropagation(); if (animalMenu) animalMenu.classList.toggle('hidden'); }); } document.addEventListener('click', (e) => { if (animalMenu && !animalMenu.classList.contains('hidden')) { if (!animalMenu.contains(e.target) && animalSwitcherBtn && !animalSwitcherBtn.contains(e.target)) { animalMenu.classList.add('hidden'); } } }); if (animalCards.length > 0) { animalCards.forEach(card => { card.addEventListener('click', () => { const index = parseInt(card.getAttribute('data-animal-index')); if (isNaN(index)) return; // Hide menu if (animalMenu) animalMenu.classList.add('hidden'); // Hide current const currentAnim = document.getElementById(animals[currentAnimalIndex].id); if (currentAnim) { currentAnim.classList.add('hidden'); currentAnim.classList.add('paused'); } // Set index currentAnimalIndex = index; // Show next const nextAnim = document.getElementById(animals[currentAnimalIndex].id); if (nextAnim) { nextAnim.classList.remove('hidden'); // sync pause state setAnimalAnimationPaused(!isRunning); } // Update label if (animalNameLabel) { animalNameLabel.textContent = animals[currentAnimalIndex].name; } }); }); } // ========================================== // PET CLOSE / SHOW & DRAG LEFT // ========================================== const trackerWrapper = document.querySelector('.task-tracker-wrapper'); const petCloseBtn = document.getElementById('petCloseBtn'); const showPetBtn = document.getElementById('showPetBtn'); if (petCloseBtn && trackerWrapper && showPetBtn) { petCloseBtn.addEventListener('click', (e) => { e.stopPropagation(); trackerWrapper.classList.add('pet-hidden'); // Close animal menu too if (animalMenu) animalMenu.classList.add('hidden'); // Show the floating paw button after transition setTimeout(() => { showPetBtn.classList.remove('hidden'); }, 300); }); showPetBtn.addEventListener('click', () => { showPetBtn.classList.add('hidden'); trackerWrapper.classList.remove('pet-hidden'); }); } // Drag tracker widget horizontally (to the left) if (trackerWrapper) { let isDraggingWidget = false; let widgetDragStartX = 0; let widgetDragStartY = 0; let widgetStartRight = 20; let widgetStartBottom = 20; let wasDragged = false; function getWidgetPosition() { const style = window.getComputedStyle(trackerWrapper); const right = parseInt(style.right) || 20; const bottom = parseInt(style.bottom) || 20; return { right, bottom }; } function startWidgetDrag(clientX, clientY) { isDraggingWidget = true; wasDragged = false; widgetDragStartX = clientX; widgetDragStartY = clientY; const pos = getWidgetPosition(); widgetStartRight = pos.right; widgetStartBottom = pos.bottom; trackerWrapper.style.transition = 'none'; } function moveWidgetDrag(clientX, clientY) { if (!isDraggingWidget) return; const deltaX = clientX - widgetDragStartX; const deltaY = clientY - widgetDragStartY; if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) { wasDragged = true; } // Moving left = increasing right offset (since widget is right-anchored) let newRight = widgetStartRight - deltaX; let newBottom = widgetStartBottom - deltaY; // Clamp to viewport const maxRight = window.innerWidth - 100; const maxBottom = window.innerHeight - 100; newRight = Math.max(-20, Math.min(maxRight, newRight)); newBottom = Math.max(10, Math.min(maxBottom, newBottom)); trackerWrapper.style.right = newRight + 'px'; trackerWrapper.style.bottom = newBottom + 'px'; } function endWidgetDrag() { if (!isDraggingWidget) return; isDraggingWidget = false; trackerWrapper.style.transition = ''; } // Mouse events for drag const hamsterContainer = trackerWrapper.querySelector('.hamster-container'); if (hamsterContainer) { hamsterContainer.addEventListener('mousedown', (e) => { if (e.target.closest('.animal-controls') || e.target.closest('button')) return; e.preventDefault(); startWidgetDrag(e.clientX, e.clientY); }); // Touch events for drag hamsterContainer.addEventListener('touchstart', (e) => { if (e.target.closest('.animal-controls') || e.target.closest('button')) return; if (e.touches.length !== 1) return; startWidgetDrag(e.touches[0].clientX, e.touches[0].clientY); }, { passive: true }); } document.addEventListener('mousemove', (e) => { if (isDraggingWidget) moveWidgetDrag(e.clientX, e.clientY); }); document.addEventListener('mouseup', () => endWidgetDrag()); document.addEventListener('touchmove', (e) => { if (isDraggingWidget && e.touches.length === 1) { moveWidgetDrag(e.touches[0].clientX, e.touches[0].clientY); } }, { passive: true }); document.addEventListener('touchend', () => endWidgetDrag()); } // ========================================== // Task Planner Logic (Pomofocus Style) // ========================================== let tasks = JSON.parse(localStorage.getItem('pomodoroTasks')) || []; let activeTaskId = localStorage.getItem('pomodoroActiveTaskId') || null; const taskPanel = document.getElementById('taskPanel'); const taskTabBtn = document.getElementById('taskTabBtn'); const closeTaskPanel = document.getElementById('closeTaskPanel'); const taskList = document.getElementById('taskList'); const addTaskTriggerBtn = document.getElementById('addTaskTriggerBtn'); const addTaskForm = document.getElementById('addTaskForm'); const cancelAddTaskBtn = document.getElementById('cancelAddTaskBtn'); const saveTaskBtn = document.getElementById('saveTaskBtn'); const taskTitleInput = document.getElementById('taskTitleInput'); const taskEstInput = document.getElementById('taskEstInput'); const estUpBtn = document.getElementById('estUpBtn'); const estDownBtn = document.getElementById('estDownBtn'); // Toggle Planner if (taskTabBtn) taskTabBtn.addEventListener('click', () => taskPanel.classList.toggle('hidden')); if (closeTaskPanel) closeTaskPanel.addEventListener('click', () => taskPanel.classList.add('hidden')); // Add Task Form Toggles if (addTaskTriggerBtn) addTaskTriggerBtn.addEventListener('click', () => { addTaskForm.classList.remove('hidden'); addTaskTriggerBtn.classList.add('hidden'); taskTitleInput.focus(); }); if (cancelAddTaskBtn) cancelAddTaskBtn.addEventListener('click', () => { addTaskForm.classList.add('hidden'); addTaskTriggerBtn.classList.remove('hidden'); taskTitleInput.value = ''; taskEstInput.value = '1'; }); // Est Input buttons if (estUpBtn) estUpBtn.addEventListener('click', () => { taskEstInput.value = parseInt(taskEstInput.value || 0) + 1; }); if (estDownBtn) estDownBtn.addEventListener('click', () => { taskEstInput.value = Math.max(1, parseInt(taskEstInput.value || 0) - 1); }); function saveTasks() { localStorage.setItem('pomodoroTasks', JSON.stringify(tasks)); if (activeTaskId) localStorage.setItem('pomodoroActiveTaskId', activeTaskId); else localStorage.removeItem('pomodoroActiveTaskId'); renderTasks(); updateActiveTaskDisplay(); } if (saveTaskBtn) saveTaskBtn.addEventListener('click', () => { const title = taskTitleInput.value.trim(); if (!title) return; const newTask = { id: Date.now().toString(), title: title, est: parseInt(taskEstInput.value) || 1, completed: 0, done: false }; tasks.push(newTask); if (!activeTaskId) activeTaskId = newTask.id; // Auto select if none saveTasks(); cancelAddTaskBtn.click(); }); // Increment active task pomodoros (Called from Timer logic) window.finishPomodoroSession = function () { // 1. Alert Sound const alertEnabled = document.getElementById('alertEnableToggle') ? document.getElementById('alertEnableToggle').checked : true; if (alertEnabled && window.playAlertSound) { window.playAlertSound(); } // 2. Stats Tracking if (window.incrementSessionStat) { window.incrementSessionStat(); } // 3. Update active task and auto-advance if (activeTaskId) { const taskIndex = tasks.findIndex(t => t.id === activeTaskId); if (taskIndex !== -1) { tasks[taskIndex].completed += 1; // Auto advance if completed meets est (or we can just mark done if completed >= est, but user requested advance when task is completed) // Let's just assume task is marked done, and advance to next undone task tasks[taskIndex].done = true; // Find next undone const nextUndone = tasks.find((t, idx) => idx > taskIndex && !t.done) || tasks.find(t => !t.done); if (nextUndone) { activeTaskId = nextUndone.id; } else { activeTaskId = null; // No tasks left } saveTasks(); } } updateActiveTaskDisplay(); }; function updateActiveTaskDisplay() { const displayEl = document.getElementById('activeTaskText'); if (!displayEl) return; let task = null; if (activeTaskId) { task = tasks.find(t => t.id === activeTaskId && !t.done); } if (!task && tasks.length > 0) { task = tasks.find(t => !t.done); if (task) { activeTaskId = task.id; } } if (task) { const taskText = task.text || task.title || task.name || task.content || ""; displayEl.textContent = taskText; return; } const phrases = [ "What do you want to focus on?", "Continue your flow?", "Ready for the next deep work block?", "Keep the momentum alive.", "Stay locked in.", "One more focused step.", "Your next win starts now." ]; // Prevent random phrase flashing on every render if we already have a valid phrase if (!phrases.includes(displayEl.textContent)) { displayEl.textContent = phrases[Math.floor(Math.random() * phrases.length)]; } } function selectTask(id) { activeTaskId = id; saveTasks(); } function toggleTaskDone(id, event) { event.stopPropagation(); const task = tasks.find(t => t.id === id); if (task) { task.done = !task.done; saveTasks(); } } function renderTasks() { if (!taskList) return; taskList.innerHTML = ''; tasks.forEach(task => { const item = document.createElement('div'); item.className = 'task-item' + (task.id === activeTaskId ? ' active' : '') + (task.done ? ' done' : ''); item.onclick = () => selectTask(task.id); const header = document.createElement('div'); header.className = 'task-header'; const checkbox = document.createElement('div'); checkbox.className = 'task-checkbox' + (task.done ? ' checked' : ''); checkbox.innerHTML = task.done ? '' : ''; checkbox.onclick = (e) => toggleTaskDone(task.id, e); const title = document.createElement('div'); title.className = 'task-title'; title.textContent = task.title; const ticks = document.createElement('div'); ticks.className = 'task-ticks'; ticks.textContent = `${task.completed} / ${task.est}`; const headerLeft = document.createElement('div'); headerLeft.className = 'task-header-left'; headerLeft.appendChild(checkbox); headerLeft.appendChild(title); const headerRight = document.createElement('div'); headerRight.className = 'task-header-right'; headerRight.appendChild(ticks); const trashIcon = document.createElement('i'); trashIcon.className = 'fas fa-trash-alt task-delete-btn'; trashIcon.onclick = (e) => { e.stopPropagation(); tasks = tasks.filter(t => t.id !== task.id); if (activeTaskId === task.id) activeTaskId = null; saveTasks(); renderTasks(); }; headerRight.appendChild(trashIcon); header.appendChild(headerLeft); header.appendChild(headerRight); item.appendChild(header); taskList.appendChild(item); }); } // Initial render renderTasks(); updateActiveTaskDisplay(); // ========================================== // UI Customizations (Scale & Position) // ========================================== // Tracker Widget Scale const trackerSizeSlider = document.getElementById('trackerSizeSlider'); if (trackerSizeSlider) { trackerSizeSlider.addEventListener('input', (e) => { document.documentElement.style.setProperty('--tracker-scale', e.target.value); }); } // Draggable Task Tab if (taskTabBtn) { let isDraggingTab = false; let startY = 0; let startTop = 0; taskTabBtn.addEventListener('mousedown', (e) => { // Only trigger drag if we don't consider it a click right away, but mousedown works. isDraggingTab = true; startY = e.clientY; const inlineTop = taskTabBtn.style.getPropertyValue('--tab-y'); startTop = parseInt(inlineTop) || 10; // Don't prevent default completely here or the click won't fire. // Or handle drag logic smartly so click still works safely. }); document.addEventListener('mousemove', (e) => { if (!isDraggingTab) return; const deltaY = e.clientY - startY; // Add a threshold before considering it a 'drag' to preserve clicks if (Math.abs(deltaY) > 2) { let newTop = startTop + deltaY; const mainHeight = document.querySelector('.tracker-main')?.clientHeight || 260; const tabHeight = taskTabBtn.clientHeight || 80; // Allow bounds if (newTop < 0) newTop = 0; if (newTop > mainHeight - tabHeight + 20) newTop = mainHeight - tabHeight + 20; taskTabBtn.style.setProperty('--tab-y', `${newTop}px`); } }); document.addEventListener('mouseup', () => { isDraggingTab = false; }); } // ========================================== // NOTION-STYLE CALENDAR & EVENT SYSTEM (PHASE 2) // ========================================== const calendarToggle = document.getElementById('calendarToggle'); const calendarOverlay = document.getElementById('calendarOverlay'); const closeCalendarBtn = document.getElementById('closeCalendarBtn'); const calDaysHeader = document.getElementById('calDaysHeader'); const calTimeCol = document.getElementById('calTimeCol'); const calGridLines = document.getElementById('calGridLines'); const calGrid = document.getElementById('calGrid'); const calEventsLayer = document.getElementById('calEventsLayer'); const eventEditorModal = document.getElementById('eventEditorModal'); const closeEventModal = document.getElementById('closeEventModal'); const eventTitleInput = document.getElementById('eventTitleInput'); const saveEventBtn = document.getElementById('saveEventBtn'); const deleteEventBtn = document.getElementById('deleteEventBtn'); const calViewSelect = document.getElementById('calViewSelect'); const calCurrentViewTitle = document.getElementById('calCurrentViewTitle'); const calPrevBtn = document.getElementById('calPrevBtn'); const calTodayBtn = document.getElementById('calTodayBtn'); const calNextBtn = document.getElementById('calNextBtn'); let notionEvents = JSON.parse(localStorage.getItem('notionEvents')) || []; // Sanitize NaN values from stored events notionEvents = notionEvents.map(ev => { if (!ev.title || ev.title === 'NaN' || ev.title === 'undefined') ev.title = ''; if (!ev.notes || ev.notes === 'NaN' || ev.notes === 'undefined') ev.notes = ''; if (isNaN(ev.startHour)) ev.startHour = 9; if (isNaN(ev.duration) || ev.duration <= 0) ev.duration = 1; return ev; }); let currentEditingEventId = null; let newEventDateStr = ''; let newEventHour = 0; let currentCalView = 'week'; // day, week, month let currentDate = new Date(); let calHourHeight = 60; // Global hour height for zoom function updateCalendarTitle() { const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; if (currentCalView === 'month') { calCurrentViewTitle.textContent = `${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`; } else if (currentCalView === 'day') { calCurrentViewTitle.textContent = `${months[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`; } else { calCurrentViewTitle.textContent = `Week of ${months[currentDate.getMonth()]} ${currentDate.getFullYear()}`; } } function initCalendarUI() { updateCalendarTitle(); calDaysHeader.innerHTML = '
'; calTimeCol.innerHTML = ''; calGrid.className = 'cal-grid'; // reset const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; // Helper: 0 for Monday, 6 for Sunday const getMonIndex = (d) => (d.getDay() + 6) % 7; if (currentCalView === 'month') { calGrid.classList.add('month-view-grid'); calDaysHeader.classList.add('month-header'); calDaysHeader.innerHTML = ''; days.forEach(d => { calDaysHeader.innerHTML += `
${d}
`; }); // simple 35-block month grid calGrid.innerHTML = ''; let tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); let startDay = getMonIndex(tempDate); tempDate.setDate(tempDate.getDate() - startDay); // roll back to Monday for (let i = 0; i < 35; i++) { const isToday = tempDate.toDateString() === new Date().toDateString(); const dateS = getLocalDateKey(tempDate); calGrid.innerHTML += `
${tempDate.getDate()}
`; tempDate.setDate(tempDate.getDate() + 1); } } else { // Day / Week view calDaysHeader.classList.remove('month-header'); const numCols = currentCalView === 'day' ? 1 : 7; calGrid.style.gridTemplateColumns = `repeat(${numCols}, 1fr)`; let startD = new Date(currentDate); if (currentCalView === 'week') { startD.setDate(startD.getDate() - getMonIndex(startD)); } for (let i = 0; i < numCols; i++) { const isToday = startD.toDateString() === new Date().toDateString(); calDaysHeader.innerHTML += `
${days[getMonIndex(startD)]} ${startD.getDate()}
`; startD.setDate(startD.getDate() + 1); } for (let i = 0; i < 24; i++) { let label = i === 0 ? '12 AM' : i < 12 ? i + ' AM' : i === 12 ? '12 PM' : (i - 12) + ' PM'; calTimeCol.innerHTML += `
${label}
`; } // Restore elements calGrid.innerHTML = `
`; } renderEvents(); } function renderEvents() { if (currentCalView === 'month') { const containers = document.querySelectorAll('.month-events-container'); containers.forEach(c => c.innerHTML = ''); notionEvents.forEach(ev => { if (!ev.dateStr) return; const container = document.getElementById(`month-ev-${ev.dateStr}`); if (container) { const block = document.createElement('div'); block.className = 'month-event-dot'; block.textContent = ev.title || 'Untitled'; block.addEventListener('click', (e) => { e.stopPropagation(); openEventModal(ev.id); }); container.appendChild(block); } }); return; } const layer = document.getElementById('calEventsLayer'); if (!layer) return; layer.innerHTML = ''; const numCols = currentCalView === 'day' ? 1 : 7; const colWidth = 100 / numCols; const hourHeight = calHourHeight; let startD = new Date(currentDate); if (currentCalView === 'week') { const getMonIndexLocal = (d) => (d.getDay() + 6) % 7; startD.setDate(startD.getDate() - getMonIndexLocal(startD)); } const visibleDates = []; for (let i = 0; i < numCols; i++) { visibleDates.push(getLocalDateKey(startD)); startD.setDate(startD.getDate() + 1); } notionEvents.forEach(ev => { let evDateStr = ev.dateStr; if (!evDateStr && ev.dayIndex !== undefined) { evDateStr = visibleDates[ev.dayIndex] || visibleDates[0]; ev.dateStr = evDateStr; } const colIndex = visibleDates.indexOf(evDateStr); if (colIndex === -1) return; const block = document.createElement('div'); block.className = 'cal-event-block'; block.style.top = (ev.startHour * hourHeight) + 'px'; block.style.height = (ev.duration * hourHeight) + 'px'; block.style.left = (colIndex * colWidth) + '%'; block.style.width = colWidth + '%'; block.innerHTML = `
${av(ev.title, 'No Title')}
`; // Dragging & Resizing Physics (Mouse) block.addEventListener('mousedown', (e) => startEventInteraction(e, block, ev, visibleDates)); // Dragging & Resizing Physics (Touch) block.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; const touch = e.touches[0]; const fakeEvent = { clientX: touch.clientX, clientY: touch.clientY, target: document.elementFromPoint(touch.clientX, touch.clientY) || e.target, stopPropagation: () => e.stopPropagation(), preventDefault: () => e.preventDefault() }; startEventInteraction(fakeEvent, block, ev, visibleDates); }, { passive: false }); layer.appendChild(block); }); } // Interactive dragging let isDraggingEvent = false; let isResizingEvent = false; let activeDragEventId = null; let activeDragBlock = null; let dragStartY = 0; let dragStartX = 0; let dragStartTop = 0; let dragStartLeftRaw = ''; let dragStartHeight = 0; let currentVisibleDatesForDrag = []; function startEventInteraction(e, block, ev, visibleDates) { e.stopPropagation(); activeDragEventId = ev.id; activeDragBlock = block; dragStartY = e.clientY; dragStartX = e.clientX; dragStartTop = parseFloat(block.style.top); dragStartLeftRaw = block.style.left; dragStartHeight = parseFloat(block.style.height); currentVisibleDatesForDrag = visibleDates; if (e.target.classList.contains('event-resize-handle')) { isResizingEvent = true; } else { isDraggingEvent = true; block.classList.add('dragging'); } } document.addEventListener('mousemove', (e) => { if ((isDraggingEvent || isResizingEvent) && activeDragBlock) { handleEventDragMove(e.clientX, e.clientY); } }); document.addEventListener('mouseup', (e) => { finishEventInteraction(e); }); // Touch move/end for event dragging document.addEventListener('touchmove', (e) => { if ((isDraggingEvent || isResizingEvent) && activeDragBlock && e.touches.length === 1) { e.preventDefault(); const touch = e.touches[0]; handleEventDragMove(touch.clientX, touch.clientY); } }, { passive: false }); document.addEventListener('touchend', (e) => { if (isDraggingEvent || isResizingEvent) { const touch = e.changedTouches[0]; const fakeEvent = { clientX: touch.clientX, clientY: touch.clientY }; finishEventInteraction(fakeEvent); } }); function handleEventDragMove(clientX, clientY) { if (isDraggingEvent && activeDragBlock) { let deltaY = clientY - dragStartY; let deltaX = clientX - dragStartX; let newTop = Math.max(0, dragStartTop + deltaY); let snapUnit = calHourHeight / 4; newTop = Math.round(newTop / snapUnit) * snapUnit; activeDragBlock.style.top = newTop + 'px'; if (currentCalView === 'week') { const gridRect = document.getElementById('calGrid').getBoundingClientRect(); let newX = dragStartX + deltaX - gridRect.left; let colIndex = Math.floor(newX / (gridRect.width / 7)); colIndex = Math.max(0, Math.min(6, colIndex)); activeDragBlock.style.left = (colIndex * (100 / 7)) + '%'; } } else if (isResizingEvent && activeDragBlock) { let deltaY = clientY - dragStartY; let snapUnit = calHourHeight / 4; let newHeight = Math.max(snapUnit, dragStartHeight + deltaY); newHeight = Math.round(newHeight / snapUnit) * snapUnit; activeDragBlock.style.height = newHeight + 'px'; } } function finishEventInteraction(e) { if ((isDraggingEvent || isResizingEvent) && activeDragBlock && activeDragEventId) { const ev = notionEvents.find(x => x.id === activeDragEventId); if (ev) { ev.startHour = parseFloat(activeDragBlock.style.top) / calHourHeight; ev.duration = parseFloat(activeDragBlock.style.height) / calHourHeight; if (isDraggingEvent && currentCalView === 'week') { const colIndex = Math.round(parseFloat(activeDragBlock.style.left) / (100 / 7)); if (currentVisibleDatesForDrag[colIndex]) { ev.dateStr = currentVisibleDatesForDrag[colIndex]; } } localStorage.setItem('notionEvents', JSON.stringify(notionEvents)); } activeDragBlock.classList.remove('dragging'); const wasDragged = Math.abs(e.clientY - dragStartY) >= 3 || Math.abs(e.clientX - dragStartX) >= 3; isDraggingEvent = false; isResizingEvent = false; activeDragEventId = null; activeDragBlock = null; currentVisibleDatesForDrag = []; if (!wasDragged && ev) { openEventModal(ev.id); } else { renderEvents(); } } } function av(val, def) { return val && val.trim() !== '' ? val : def; } if (calendarToggle) { calendarToggle.addEventListener('click', () => { calendarOverlay.classList.remove('hidden'); initCalendarUI(); }); } if (closeCalendarBtn) { closeCalendarBtn.addEventListener('click', () => { calendarOverlay.classList.add('hidden'); }); } if (calViewSelect) { calViewSelect.addEventListener('change', (e) => { currentCalView = e.target.value; initCalendarUI(); }); } if (calPrevBtn) calPrevBtn.addEventListener('click', () => { if (currentCalView === 'day') currentDate.setDate(currentDate.getDate() - 1); else if (currentCalView === 'week') currentDate.setDate(currentDate.getDate() - 7); else currentDate.setMonth(currentDate.getMonth() - 1); initCalendarUI(); }); if (calNextBtn) calNextBtn.addEventListener('click', () => { if (currentCalView === 'day') currentDate.setDate(currentDate.getDate() + 1); else if (currentCalView === 'week') currentDate.setDate(currentDate.getDate() + 7); else currentDate.setMonth(currentDate.getMonth() + 1); initCalendarUI(); }); if (calTodayBtn) calTodayBtn.addEventListener('click', () => { currentDate = new Date(); initCalendarUI(); }); // Click Grid to Create Event if (calGrid) { calGrid.addEventListener('click', (e) => { if (e.target.closest('.cal-event-block') || e.target.closest('.month-event-dot')) return; if (currentCalView === 'month') { const monthCell = e.target.closest('.month-day-cell'); if (monthCell) { newEventDateStr = monthCell.getAttribute('data-datestr'); newEventHour = new Date().getHours() % 24; // default near current time openEventModal(null); } return; } const rect = calGrid.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; const numCols = currentCalView === 'day' ? 1 : 7; const dayIndex = Math.floor(clickX / (rect.width / numCols)); const startHour = Math.floor(clickY / calHourHeight); let targetD = new Date(currentDate); if (currentCalView === 'week') { const getMonIndexLocal = (d) => (d.getDay() + 6) % 7; targetD.setDate(targetD.getDate() - getMonIndexLocal(targetD) + Math.max(0, Math.min(6, dayIndex))); } newEventDateStr = getLocalDateKey(targetD); newEventHour = Math.max(0, Math.min(23, startHour)); openEventModal(null); }); // Semantic Zoom gestures (Scroll / Trackpad Pinch) let lastZoomTime = 0; calGrid.addEventListener('wheel', (e) => { // Only intercept if we actually want to zoom, throttle to prevent insane scrolling const now = Date.now(); if (now - lastZoomTime < 400) return; const views = ['day', 'week', 'month']; let currentIndex = views.indexOf(currentCalView); let changed = false; if (e.deltaY > 20) { currentIndex = Math.min(2, currentIndex + 1); changed = true; } else if (e.deltaY < -20) { currentIndex = Math.max(0, currentIndex - 1); changed = true; } if (changed && views[currentIndex] !== currentCalView) { e.preventDefault(); // Only prevent default vertically if we successfully zoomed currentCalView = views[currentIndex]; calViewSelect.value = currentCalView; initCalendarUI(); renderEvents(); lastZoomTime = now; } }, { passive: false }); } // ========================================== // EVENT EDITOR & NOTION TABS // ========================================== const eventTabs = document.querySelectorAll('.event-tab-btn'); const tabPanes = document.querySelectorAll('.event-tab-pane'); const eventNotesArea = document.getElementById('eventNotesArea'); const sketchGallery = document.getElementById('sketchGallery'); let currentEventSketches = []; eventTabs.forEach(btn => { btn.addEventListener('click', () => { eventTabs.forEach(b => b.classList.remove('active')); tabPanes.forEach(p => p.classList.add('hidden')); btn.classList.add('active'); document.getElementById(btn.getAttribute('data-tab')).classList.remove('hidden'); }); }); let isStandaloneNote = false; let currentEditingNoteId = null; let standaloneNotes = JSON.parse(localStorage.getItem('fliqlow_standalone_notes')) || []; const noteToggleBtn = document.getElementById('noteToggleBtn'); const noteListDropdown = document.getElementById('noteListDropdown'); const createNewNoteBtn = document.getElementById('createNewNoteBtn'); const noteListContainer = document.getElementById('noteListContainer'); if (noteToggleBtn && noteListDropdown) { noteToggleBtn.addEventListener('click', (e) => { e.stopPropagation(); // Hide other dropdowns if needed const ambienceDrawer = document.getElementById('ambienceDrawer'); if (ambienceDrawer && !ambienceDrawer.classList.contains('hidden')) { ambienceDrawer.classList.add('hidden'); } noteListDropdown.classList.toggle('hidden'); renderNoteList(); }); document.addEventListener('click', (e) => { if (!e.target.closest('#notePanelContainer') && !e.target.closest('#eventEditorModal')) { noteListDropdown.classList.add('hidden'); } }); } if (createNewNoteBtn) { createNewNoteBtn.addEventListener('click', (e) => { e.stopPropagation(); noteListDropdown.classList.add('hidden'); openEventModal(null, true); }); } function renderNoteList() { if (!noteListContainer) return; noteListContainer.innerHTML = ''; if (standaloneNotes.length === 0) { noteListContainer.innerHTML = '
No notes yet. Click + to create one.
'; return; } // Sort by updated descending const sortedNotes = [...standaloneNotes].sort((a, b) => b.updatedAt - a.updatedAt); sortedNotes.forEach(note => { const item = document.createElement('div'); item.style.padding = '8px'; item.style.background = 'rgba(255,255,255,0.05)'; item.style.borderRadius = '6px'; item.style.cursor = 'pointer'; item.style.border = '1px solid rgba(255,255,255,0.1)'; // Preview text (strip HTML) const tempDiv = document.createElement('div'); tempDiv.innerHTML = note.content || ''; let previewText = tempDiv.textContent || tempDiv.innerText || ''; if (previewText.length > 40) previewText = previewText.substring(0, 40) + '...'; if (!previewText.trim()) previewText = 'No content'; item.innerHTML = `
${note.title || 'Untitled'}
${previewText}
${new Date(note.updatedAt).toLocaleDateString()} ${new Date(note.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
`; item.addEventListener('click', () => { noteListDropdown.classList.add('hidden'); openEventModal(note.id, true); }); noteListContainer.appendChild(item); }); } function openEventModal(eventId, isNote = false) { isStandaloneNote = isNote; eventEditorModal.classList.remove('hidden'); if (isStandaloneNote) { currentEditingNoteId = eventId; currentEditingEventId = null; } else { currentEditingEventId = eventId; currentEditingNoteId = null; } // Reset standard UI eventTitleInput.value = ''; eventNotesArea.innerHTML = ''; currentEventSketches = []; // Reset tabs eventTabs[0].click(); if (isStandaloneNote) { if (eventId) { const note = standaloneNotes.find(n => n.id === eventId); if (note) { eventTitleInput.value = note.title || ''; let safeNotes = (note.content && note.content !== 'NaN') ? note.content : ''; safeNotes = safeNotes.replace(/>NaN<'); safeNotes = safeNotes.replace(/^NaN$/, ''); eventNotesArea.innerHTML = safeNotes; currentEventSketches = note.sketches ? [...note.sketches] : []; document.getElementById('eventModalSubtitle').textContent = 'Edit Note'; if (deleteEventBtn) deleteEventBtn.classList.remove('hidden'); if (eventNotesArea.innerHTML.trim() !== '') { const hasBlocks = Array.from(eventNotesArea.children).some(c => c.classList && (c.classList.contains('notion-block') || c.classList.contains('notion-todo-item'))); if (!hasBlocks) { const wrapper = document.createElement('div'); wrapper.className = 'notion-block'; wrapper.contentEditable = 'true'; wrapper.innerHTML = eventNotesArea.innerHTML; eventNotesArea.innerHTML = ''; eventNotesArea.appendChild(wrapper); } } ensureDefaultBlock(); } } else { document.getElementById('eventModalSubtitle').textContent = 'New Note'; if (deleteEventBtn) deleteEventBtn.classList.add('hidden'); ensureDefaultBlock(); setTimeout(() => eventTitleInput.focus(), 100); } } else { if (eventId) { const ev = notionEvents.find(e => e.id === eventId); if (ev) { eventTitleInput.value = (ev.title && ev.title !== 'NaN') ? ev.title : ''; let safeNotes = (ev.notes && ev.notes !== 'NaN') ? ev.notes : ''; safeNotes = safeNotes.replace(/>NaN<'); safeNotes = safeNotes.replace(/^NaN$/, ''); eventNotesArea.innerHTML = safeNotes; currentEventSketches = ev.sketches ? [...ev.sketches] : []; document.getElementById('eventModalSubtitle').textContent = 'Edit Event'; if (deleteEventBtn) deleteEventBtn.classList.remove('hidden'); // SANITIZE: Wrap raw text in blocks to fix "unavailable" notes issue if (eventNotesArea.innerHTML.trim() !== '') { const hasBlocks = Array.from(eventNotesArea.children).some(c => c.classList && (c.classList.contains('notion-block') || c.classList.contains('notion-todo-item'))); if (!hasBlocks) { const wrapper = document.createElement('div'); wrapper.className = 'notion-block'; wrapper.contentEditable = 'true'; wrapper.innerHTML = eventNotesArea.innerHTML; eventNotesArea.innerHTML = ''; eventNotesArea.appendChild(wrapper); } } ensureDefaultBlock(); } } else { document.getElementById('eventModalSubtitle').textContent = 'New Event'; eventTitleInput.value = ''; eventNotesArea.innerHTML = ''; currentEventSketches = []; if (deleteEventBtn) deleteEventBtn.classList.add('hidden'); ensureDefaultBlock(); setTimeout(() => eventTitleInput.focus(), 100); } } renderSketchesGallery(); } if (closeEventModal) { closeEventModal.addEventListener('click', () => { eventEditorModal.classList.add('hidden'); slashMenu.classList.add('hidden'); }); } if (deleteEventBtn) { deleteEventBtn.addEventListener('click', () => { if (isStandaloneNote) { if (currentEditingNoteId) { standaloneNotes = standaloneNotes.filter(n => n.id !== currentEditingNoteId); localStorage.setItem('fliqlow_standalone_notes', JSON.stringify(standaloneNotes)); renderNoteList(); closeEventModal.click(); } } else { if (currentEditingEventId) { notionEvents = notionEvents.filter(e => e.id !== currentEditingEventId); localStorage.setItem('notionEvents', JSON.stringify(notionEvents)); renderEvents(); closeEventModal.click(); } } }); } if (saveEventBtn) { saveEventBtn.addEventListener('click', () => { if (isStandaloneNote) { if (currentEditingNoteId) { const note = standaloneNotes.find(n => n.id === currentEditingNoteId); if (note) { note.title = eventTitleInput.value; note.content = eventNotesArea.innerHTML; note.sketches = currentEventSketches; note.updatedAt = Date.now(); } } else { const newNote = { id: 'note_' + Date.now(), title: eventTitleInput.value, content: eventNotesArea.innerHTML, sketches: currentEventSketches, updatedAt: Date.now() }; standaloneNotes.push(newNote); } localStorage.setItem('fliqlow_standalone_notes', JSON.stringify(standaloneNotes)); renderNoteList(); closeEventModal.click(); } else { if (currentEditingEventId) { const ev = notionEvents.find(e => e.id === currentEditingEventId); if (ev) { ev.title = eventTitleInput.value; ev.notes = eventNotesArea.innerHTML; ev.sketches = currentEventSketches; } } else { const newEvent = { id: Date.now().toString(), dateStr: newEventDateStr, startHour: newEventHour, duration: 1, // default 1 hour block title: eventTitleInput.value, notes: eventNotesArea.innerHTML, sketches: currentEventSketches }; notionEvents.push(newEvent); } localStorage.setItem('notionEvents', JSON.stringify(notionEvents)); renderEvents(); closeEventModal.click(); } }); } // ========================================== // SLASH COMMAND MENU & TRUE BLOCK EDITOR (PHASE 3) // ========================================== const slashMenu = document.getElementById('slashMenu'); const blockAddBtn = document.getElementById('blockAddBtn'); const blockDragBtn = document.getElementById('blockDragBtn'); let activeHoverNode = null; let draggedBlock = null; // Ensure event area always has one block function ensureDefaultBlock() { const textContent = eventNotesArea.textContent.trim(); if (eventNotesArea.children.length === 0 || textContent === '' || textContent === 'NaN') { eventNotesArea.innerHTML = '
'; } } eventNotesArea.addEventListener('focusout', () => { setTimeout(ensureDefaultBlock, 100); }); ensureDefaultBlock(); // Intercept Keypresses to Manage Blocks eventNotesArea.addEventListener('keydown', (e) => { const isBlock = e.target.classList.contains('notion-block'); if (!isBlock) return; if (e.key === 'Enter' && !e.shiftKey) { const todoItem = e.target.closest('.notion-todo-item'); if (todoItem) { // We are inside a bullet or todo-list item e.preventDefault(); slashMenu.classList.add('hidden'); if (e.target.textContent.trim() === '') { // Empty item → exit list, create a plain block after the todo-item const newBlock = document.createElement('div'); newBlock.className = 'notion-block'; newBlock.contentEditable = 'true'; todoItem.after(newBlock); newBlock.focus(); } else { // Non-empty item → duplicate the item type after it const isTodo = todoItem.hasAttribute('data-checked'); const newItem = document.createElement('div'); newItem.contentEditable = 'false'; if (isTodo) { newItem.className = 'notion-todo-item'; newItem.setAttribute('data-checked', 'false'); newItem.innerHTML = '
'; } else { newItem.className = 'notion-todo-item'; newItem.innerHTML = '
â€Ē
'; } todoItem.after(newItem); const newText = newItem.querySelector('.todo-text'); if (newText) newText.focus(); } } else { // Normal paragraph block e.preventDefault(); const newBlock = document.createElement('div'); newBlock.className = 'notion-block'; newBlock.contentEditable = 'true'; slashMenu.classList.add('hidden'); e.target.after(newBlock); // Split text content if caret is in the middle const sel = window.getSelection(); if (sel.rangeCount > 0) { const range = sel.getRangeAt(0); if (range.endOffset < e.target.textContent.length) { const txt = e.target.textContent; newBlock.textContent = txt.slice(range.endOffset); e.target.textContent = txt.slice(0, range.endOffset); } } newBlock.focus(); } } else if (e.key === 'Backspace') { const todoItem = e.target.closest('.notion-todo-item'); if (e.target.textContent.length === 0) { e.preventDefault(); if (todoItem) { // Empty bullet/todo → remove the item, focus previous sibling const prev = todoItem.previousElementSibling; todoItem.remove(); if (prev) { const focusTarget = prev.classList.contains('notion-todo-item') ? prev.querySelector('.notion-block') : prev; if (focusTarget) { const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(focusTarget); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); focusTarget.focus(); } } } else { const prev = e.target.previousElementSibling; if (prev && prev.classList.contains('notion-block')) { e.target.remove(); const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(prev); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } } } } }); // Positioning Handles eventNotesArea.addEventListener('mousemove', (e) => { let node = document.elementFromPoint(e.clientX, e.clientY); if (!node) return; // Resolve block parent if (!node.classList.contains('notion-block') && !node.classList.contains('notion-todo-item')) { let parent = node.closest('.notion-block, .notion-todo-item'); if (parent) node = parent; else return; } if (node && (node.classList.contains('notion-block') || node.classList.contains('notion-todo-item'))) { activeHoverNode = node; const rect = activeHoverNode.getBoundingClientRect(); const areaRect = eventNotesArea.getBoundingClientRect(); blockAddBtn.classList.remove('hidden'); blockDragBtn.classList.remove('hidden'); blockAddBtn.style.top = (rect.top - areaRect.top + 2) + 'px'; blockDragBtn.style.top = (rect.top - areaRect.top + 2) + 'px'; } }); eventNotesArea.addEventListener('mouseleave', (e) => { if (e.relatedTarget && e.relatedTarget.closest('.event-notes-wrapper')) return; blockAddBtn.classList.add('hidden'); blockDragBtn.classList.add('hidden'); }); // Handle Slash Popup Location blockAddBtn.addEventListener('click', (e) => { e.stopPropagation(); if (activeHoverNode) { const btnRect = blockAddBtn.getBoundingClientRect(); slashMenu.style.left = (btnRect.right + 8) + 'px'; slashMenu.style.top = btnRect.top + 'px'; slashMenu.classList.remove('hidden'); } }); function getCaretCoordinates() { let x = 0, y = 0; const isSupported = typeof window.getSelection !== "undefined"; if (isSupported) { const selection = window.getSelection(); if (selection.rangeCount !== 0) { const range = selection.getRangeAt(0).cloneRange(); range.collapse(true); const rect = range.getClientRects()[0]; if (rect) { x = rect.left; y = rect.top; } } } return { x, y }; } eventNotesArea.addEventListener('keyup', (e) => { if (e.key === '/') { const coords = getCaretCoordinates(); slashMenu.style.left = Math.max(30, coords.x - 20) + 'px'; slashMenu.style.top = (coords.y + 20) + 'px'; slashMenu.classList.remove('hidden'); } else if (e.key === 'Escape') { slashMenu.classList.add('hidden'); } }); document.addEventListener('click', (e) => { if (!slashMenu.classList.contains('hidden') && !e.target.closest('#slashMenu') && !e.target.closest('#blockAddBtn')) { slashMenu.classList.add('hidden'); } }); const slashItems = document.querySelectorAll('.slash-item'); slashItems.forEach(item => { item.addEventListener('click', () => { const action = item.getAttribute('data-action'); slashMenu.classList.add('hidden'); // Inject format safely utilizing activeHoverNode const targetNode = activeHoverNode || eventNotesArea.lastElementChild || eventNotesArea; // Clear text if it's just formatting targetNode.textContent = targetNode.textContent.replace('/', ''); if (action === 'todo') { const todoHtml = `
`; targetNode.outerHTML = todoHtml; } else if (action === 'h2') { targetNode.outerHTML = `
`; } else if (action === 'h3') { targetNode.outerHTML = `
`; } else if (action === 'bullet') { targetNode.outerHTML = `
â€Ē
`; } // Setup focus for new element setTimeout(() => { const created = eventNotesArea.querySelector('.notion-block[data-placeholder]'); if (created) created.focus(); }, 50); }); }); // Drag and Drop Rearrangement (Node swapping) blockDragBtn.setAttribute('draggable', 'true'); blockDragBtn.addEventListener('dragstart', (e) => { if (activeHoverNode) { draggedBlock = activeHoverNode; setTimeout(() => draggedBlock.style.opacity = '0.5', 0); // Set required data for Firefox e.dataTransfer.setData('text/plain', ''); } }); blockDragBtn.addEventListener('dragend', () => { if (draggedBlock) draggedBlock.style.opacity = '1'; draggedBlock = null; }); eventNotesArea.addEventListener('dragover', (e) => { e.preventDefault(); if (!draggedBlock) return; const target = e.target.closest('.notion-block, .notion-todo-item'); if (target && target !== draggedBlock && target.parentNode === eventNotesArea) { const rect = target.getBoundingClientRect(); const after = e.clientY > rect.top + (rect.height / 2); if (after) { target.after(draggedBlock); } else { target.before(draggedBlock); } } }); // Restore Checkbox Interactive Logic & Empty Space Focus eventNotesArea.addEventListener('click', (e) => { if (e.target === eventNotesArea) { // If clicking inside the wrapper but outside blocks, focus last block const lastChild = eventNotesArea.lastElementChild; if (lastChild && (lastChild.classList.contains('notion-block') || lastChild.classList.contains('notion-todo-item'))) { const blockToFocus = lastChild.classList.contains('notion-todo-item') ? lastChild.querySelector('.notion-block') : lastChild; if (blockToFocus) { const range = document.createRange(); range.selectNodeContents(blockToFocus); range.collapse(false); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); blockToFocus.focus(); } } return; } if (e.target.closest('.todo-checkbox-btn')) { e.preventDefault(); const todoItem = e.target.closest('.notion-todo-item'); if (todoItem) { const isChecked = todoItem.getAttribute('data-checked') === 'true'; todoItem.setAttribute('data-checked', isChecked ? 'false' : 'true'); const btn = todoItem.querySelector('.todo-checkbox-btn'); if (btn) btn.innerHTML = isChecked ? '' : ''; } } }); // ========================================== // GOODNOTES-STYLE CANVAS SKETCHER // ========================================== const canvasOverlay = document.getElementById('canvasOverlay'); const addSketchPageBtn = document.getElementById('addSketchPageBtn'); const cancelCanvasBtn = document.getElementById('cancelCanvasBtn'); const saveCanvasBtn = document.getElementById('saveCanvasBtn'); const clearCanvasBtn = document.getElementById('clearCanvasBtn'); const sketchCanvas = document.getElementById('sketchCanvas'); const sketchCtx = sketchCanvas.getContext('2d'); const penToolBtn = document.getElementById('penToolBtn'); const eraserToolBtn = document.getElementById('eraserToolBtn'); const penColorPicker = document.getElementById('penColorPicker'); const penSizeSlider = document.getElementById('penSizeSlider'); let isDrawingCanvas = false; let currentTool = 'pen'; // 'pen' | 'eraser' function renderSketchesGallery() { // Clear all except the "Add Blank Page" button Array.from(sketchGallery.children).forEach(child => { if (child.id !== 'addSketchPageBtn') { child.remove(); } }); currentEventSketches.forEach((sketchData, index) => { const card = document.createElement('div'); card.className = 'sketch-card'; card.innerHTML = ` Sketch `; sketchGallery.insertBefore(card, addSketchPageBtn); card.querySelector('.sketch-card-delete').addEventListener('click', (e) => { e.stopPropagation(); currentEventSketches.splice(index, 1); renderSketchesGallery(); }); }); } function resizeCanvas() { // make it large for sketching sketchCanvas.width = document.querySelector('.canvas-workspace').clientWidth - 40; sketchCanvas.height = document.querySelector('.canvas-workspace').clientHeight - 40; // Fill white or leave transparent // Best is to leave transparent so it looks natural on dark mode, but let's give it an off-black background. sketchCtx.fillStyle = '#111111'; sketchCtx.fillRect(0, 0, sketchCanvas.width, sketchCanvas.height); } if (addSketchPageBtn) { addSketchPageBtn.addEventListener('click', () => { canvasOverlay.classList.remove('hidden'); // Delay to allow DOM update for clientWidth setTimeout(resizeCanvas, 50); }); } if (cancelCanvasBtn) { cancelCanvasBtn.addEventListener('click', () => { canvasOverlay.classList.add('hidden'); }); } if (clearCanvasBtn) { clearCanvasBtn.addEventListener('click', () => { sketchCtx.fillStyle = '#111111'; sketchCtx.fillRect(0, 0, sketchCanvas.width, sketchCanvas.height); }); } if (saveCanvasBtn) { saveCanvasBtn.addEventListener('click', () => { const dataUrl = sketchCanvas.toDataURL('image/jpeg', 0.8); currentEventSketches.push(dataUrl); renderSketchesGallery(); canvasOverlay.classList.add('hidden'); }); } // Tool switching penToolBtn.addEventListener('click', () => { currentTool = 'pen'; penToolBtn.classList.add('active'); eraserToolBtn.classList.remove('active'); }); eraserToolBtn.addEventListener('click', () => { currentTool = 'eraser'; eraserToolBtn.classList.add('active'); penToolBtn.classList.remove('active'); }); // Drawing mechanics function startPosition(e) { isDrawingCanvas = true; draw(e); } function finishPosition() { isDrawingCanvas = false; sketchCtx.beginPath(); } function draw(e) { if (!isDrawingCanvas) return; // Support mouse & touch (simplified for mouse here) const rect = sketchCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; sketchCtx.lineWidth = penSizeSlider.value; sketchCtx.lineCap = 'round'; if (currentTool === 'eraser') { sketchCtx.strokeStyle = '#111111'; // background color to 'erase' } else { sketchCtx.strokeStyle = penColorPicker.value; } sketchCtx.lineTo(x, y); sketchCtx.stroke(); sketchCtx.beginPath(); sketchCtx.moveTo(x, y); } sketchCanvas.addEventListener('mousedown', startPosition); sketchCanvas.addEventListener('mouseup', finishPosition); sketchCanvas.addEventListener('mousemove', draw); sketchCanvas.addEventListener('mouseleave', finishPosition); // Pinch Zoom Logic for Calendar (iOS & Desktop) let initialPinchDist = null; let initialHourHeight = calHourHeight; const calBodyEl = document.querySelector('.calendar-body'); if (calBodyEl) { calBodyEl.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { // Prevent native pinch zoom e.preventDefault(); calBodyEl.classList.add('pinching'); if (currentCalView !== 'month') { initialPinchDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); initialHourHeight = calHourHeight; } } }, { passive: false }); calBodyEl.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { e.preventDefault(); if (currentCalView === 'month') { // In month view, pinch can switch views return; } if (initialPinchDist !== null) { const currentDist = Math.hypot( e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY ); const scale = currentDist / initialPinchDist; calHourHeight = Math.max(30, Math.min(150, initialHourHeight * scale)); // Update time label heights to match const timeLabels = document.querySelectorAll('.cal-time-label'); timeLabels.forEach(label => { label.style.height = calHourHeight + 'px'; }); // Update grid background lines const calGridEl = document.getElementById('calGrid'); if (calGridEl) { const lineH = calHourHeight - 1; calGridEl.style.backgroundImage = `repeating-linear-gradient(to bottom, transparent, transparent ${lineH}px, rgba(255,255,255,0.05) ${lineH}px, rgba(255,255,255,0.05) ${calHourHeight}px)`; } renderEvents(); } } }, { passive: false }); calBodyEl.addEventListener('touchend', (e) => { if (e.touches.length < 2) { initialPinchDist = null; calBodyEl.classList.remove('pinching'); } }); // Gesturestart/gesturechange for Safari (additional iOS support) calBodyEl.addEventListener('gesturestart', (e) => { e.preventDefault(); }); calBodyEl.addEventListener('gesturechange', (e) => { e.preventDefault(); }); calBodyEl.addEventListener('gestureend', (e) => { e.preventDefault(); }); } // ========================================== // PREMIUM UPGRADE LOGIC // ========================================== // Ambience Audio toggle — handler registered below in AMBIENCE TOGGLE LOGIC section const ambienceToggleBtnSidebar = document.getElementById('ambienceToggleBtn'); const ambienceDrawerSidebar = document.getElementById('ambienceDrawer'); // Task Panel toggle const taskPanelToggleBtn = document.getElementById('taskPanelToggleBtn'); const taskPanelSidebar = document.getElementById('taskPanel'); const closeTaskPanelSidebar = document.getElementById('closeTaskPanel'); if (taskPanelToggleBtn && taskPanelSidebar) { taskPanelToggleBtn.addEventListener('click', (e) => { e.stopPropagation(); taskPanelSidebar.classList.toggle('hidden'); taskPanelToggleBtn.classList.toggle('active', !taskPanelSidebar.classList.contains('hidden')); }); } if (closeTaskPanelSidebar && taskPanelSidebar) { closeTaskPanelSidebar.addEventListener('click', () => { taskPanelSidebar.classList.add('hidden'); if (taskPanelToggleBtn) taskPanelToggleBtn.classList.remove('active'); }); } // Alert Audio Context setup let alertAudioCtx = null; const alertVolumeSlider = document.getElementById('alertVolumeSlider'); const alertToggleBtn = document.getElementById('alertToggleBtn'); const alertDrawer = document.getElementById('alertDrawer'); if (alertToggleBtn) { alertToggleBtn.addEventListener('click', () => { alertDrawer.classList.toggle('hidden'); alertToggleBtn.classList.toggle('active', !alertDrawer.classList.contains('hidden')); }); } document.addEventListener('click', (e) => { if (alertDrawer && !alertDrawer.classList.contains('hidden') && !e.target.closest('#alertPanel')) { alertDrawer.classList.add('hidden'); if (alertToggleBtn) alertToggleBtn.classList.remove('active'); } if (ambienceDrawerSidebar && !ambienceDrawerSidebar.classList.contains('hidden') && !e.target.closest('#ambienceDrawer') && !e.target.closest('#ambienceToggleBtn')) { ambienceDrawerSidebar.classList.add('hidden'); if (ambienceToggleBtnSidebar) ambienceToggleBtnSidebar.classList.remove('active'); } }); window.playAlertSound = function () { if (!alertAudioCtx) { alertAudioCtx = new (window.AudioContext || window.webkitAudioContext)(); } let vol = alertVolumeSlider ? parseInt(alertVolumeSlider.value) / 100 : 0.5; if (vol === 0) return; // A pleasant soft double-chime function playChime(freq, startTime, duration) { const osc = alertAudioCtx.createOscillator(); const gainNode = alertAudioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(freq, startTime); gainNode.gain.setValueAtTime(0, startTime); gainNode.gain.linearRampToValueAtTime(vol * 0.5, startTime + 0.05); gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration); osc.connect(gainNode); gainNode.connect(alertAudioCtx.destination); osc.start(startTime); osc.stop(startTime + duration); } const t = alertAudioCtx.currentTime; playChime(880, t, 1.5); // A5 playChime(1108.73, t + 0.15, 1.5); // C#6 }; // ========================================== // FOCUS STATS TRACKING (REWRITE) // ========================================== function getLocalDateKey(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } function formatSeconds(totalSec) { const hrs = Math.floor(totalSec / 3600); const mins = Math.floor((totalSec % 3600) / 60); if (hrs > 0) return hrs + 'h ' + mins + 'm'; return mins + 'm'; } const DEFAULT_STATS = { totalAppSeconds: 0, totalFocusSeconds: 0, totalStartClicks: 0, completedFocusSessions: 0, currentStreak: 0, longestStreak: 0, lastSessionDate: null, daily: {} }; let focusStats = (() => { try { const raw = JSON.parse(localStorage.getItem('fliqlow_stats_v2')); if (raw && typeof raw.totalAppSeconds === 'number') return { ...DEFAULT_STATS, ...raw }; } catch (e) { } // Migrate from old stats if present try { const old = JSON.parse(localStorage.getItem('fliqlow_stats')); if (old) { return { ...DEFAULT_STATS, totalFocusSeconds: old.totalFocusSeconds || 0, totalStartClicks: old.startClicks || 0, completedFocusSessions: old.totalSessions || 0, currentStreak: old.streak || 0, longestStreak: old.streak || 0, daily: {} }; } } catch (e) { } return { ...DEFAULT_STATS }; })(); function saveStats() { // Prune daily entries older than 30 days const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 30); const cutoffKey = getLocalDateKey(cutoff); Object.keys(focusStats.daily).forEach(k => { if (k < cutoffKey) delete focusStats.daily[k]; }); localStorage.setItem('fliqlow_stats_v2', JSON.stringify(focusStats)); } function ensureDailyEntry() { const today = getLocalDateKey(new Date()); if (!focusStats.daily[today]) { focusStats.daily[today] = { appSeconds: 0, focusSeconds: 0, startClicks: 0, completedFocusSessions: 0 }; } return today; } // App time tracking (visibility-aware, 1s tick) let _appTimeInterval = null; function startAppTimeTracking() { if (_appTimeInterval) return; _appTimeInterval = setInterval(() => { if (document.visibilityState === 'visible') { focusStats.totalAppSeconds++; const today = ensureDailyEntry(); focusStats.daily[today].appSeconds++; // Also track focus time if running pomodoro if (isRunning && currentMode === 'pomodoro') { focusStats.totalFocusSeconds++; focusStats.daily[today].focusSeconds++; } // Save every 30 seconds to avoid excessive writes if (focusStats.totalAppSeconds % 30 === 0) saveStats(); } }, 1000); } startAppTimeTracking(); // Save stats on page unload window.addEventListener('beforeunload', saveStats); // Track start clicks (called from toggleTimer) function trackStartClick() { focusStats.totalStartClicks++; const today = ensureDailyEntry(); focusStats.daily[today].startClicks++; saveStats(); } // Completed session tracking window.incrementSessionStat = function () { focusStats.completedFocusSessions++; const today = ensureDailyEntry(); focusStats.daily[today].completedFocusSessions++; // Streak logic: each completed session increments streak focusStats.currentStreak++; if (focusStats.currentStreak > focusStats.longestStreak) { focusStats.longestStreak = focusStats.currentStreak; } focusStats.lastSessionDate = today; saveStats(); renderStats(); renderSessionTally(); updateStreakIndicator(); }; function updateStreakIndicator() { const el = document.getElementById('streakCount'); const container = document.getElementById('streakIndicator'); if (el) el.textContent = focusStats.currentStreak; if (container) { container.classList.toggle('has-streak', focusStats.currentStreak > 0); } } updateStreakIndicator(); const statsModal = document.getElementById('statsModal'); const statsToggleBtn = document.getElementById('statsToggleBtn'); const closeStats = document.getElementById('closeStats'); const mobileCloseStats = document.getElementById('mobileCloseStats'); if (statsToggleBtn) statsToggleBtn.addEventListener('click', () => { renderStats(); statsModal.classList.remove('hidden'); }); if (closeStats) closeStats.addEventListener('click', () => statsModal.classList.add('hidden')); if (mobileCloseStats) mobileCloseStats.addEventListener('click', () => statsModal.classList.add('hidden')); function renderStats() { const appTimeEl = document.getElementById('statTotalAppTime'); if (appTimeEl) appTimeEl.textContent = formatSeconds(focusStats.totalAppSeconds); const focusTimeEl = document.getElementById('statTotalFocusTime'); if (focusTimeEl) focusTimeEl.textContent = formatSeconds(focusStats.totalFocusSeconds); const clicksEl = document.getElementById('statStartClicks'); if (clicksEl) clicksEl.textContent = focusStats.totalStartClicks; const sessionsEl = document.getElementById('statSessionsCompleted'); if (sessionsEl) sessionsEl.textContent = focusStats.completedFocusSessions; const streakEl = document.getElementById('statCurrentStreak'); if (streakEl) streakEl.textContent = focusStats.currentStreak; const longestEl = document.getElementById('statLongestStreak'); if (longestEl) longestEl.textContent = focusStats.longestStreak; renderProductivityChart(); } function renderProductivityChart() { const canvas = document.getElementById('productivityGraph'); if (!canvas) return; const ctx2 = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const rect = canvas.parentElement.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = 200 * dpr; canvas.style.width = rect.width + 'px'; canvas.style.height = '200px'; ctx2.scale(dpr, dpr); const W = rect.width; const H = 200; ctx2.clearRect(0, 0, W, H); // Build last 7 days data from daily stats const today = new Date(); today.setHours(0, 0, 0, 0); const dailySeconds = []; const dayLabels = []; for (let i = 6; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); const key = getLocalDateKey(d); const entry = focusStats.daily[key]; dailySeconds.push(entry ? entry.focusSeconds : 0); dayLabels.push(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); } let maxSec = Math.max(...dailySeconds); if (maxSec === 0) maxSec = 3600; // Default 1h // Smart Y-axis const maxHrs = maxSec / 3600; let yMax; if (maxHrs <= 1) yMax = 1; else if (maxHrs <= 2) yMax = 2; else if (maxHrs <= 5) yMax = 5; else if (maxHrs <= 10) yMax = 10; else if (maxHrs <= 24) yMax = 24; else yMax = Math.ceil(maxHrs / 5) * 5; const yMaxSec = yMax * 3600; const padding = { top: 15, right: 15, bottom: 30, left: 50 }; const chartW = W - padding.left - padding.right; const chartH = H - padding.top - padding.bottom; const barW = (chartW / 7) * 0.5; const barGap = (chartW / 7); // Y-axis ctx2.fillStyle = '#666'; ctx2.font = '11px Inter, sans-serif'; ctx2.textAlign = 'right'; for (let i = 0; i <= 4; i++) { const y = padding.top + chartH - (chartH * i / 4); const val = (yMax * i / 4); ctx2.fillText(val >= 1 ? val + 'h' : Math.round(val * 60) + 'm', padding.left - 8, y + 4); ctx2.strokeStyle = 'rgba(255,255,255,0.05)'; ctx2.lineWidth = 1; ctx2.beginPath(); ctx2.moveTo(padding.left, y); ctx2.lineTo(W - padding.right, y); ctx2.stroke(); } // Bars dailySeconds.forEach((sec, idx) => { const barH = (sec / yMaxSec) * chartH; const x = padding.left + (idx * barGap) + (barGap - barW) / 2; const y = padding.top + chartH - barH; const gradient = ctx2.createLinearGradient(x, y + barH, x, y); gradient.addColorStop(0, 'rgba(99, 102, 241, 0.8)'); gradient.addColorStop(1, 'rgba(168, 85, 247, 0.9)'); ctx2.fillStyle = barH > 0 ? gradient : 'rgba(255,255,255,0.03)'; ctx2.beginPath(); ctx2.roundRect(x, barH > 0 ? y : padding.top + chartH - 2, barW, Math.max(barH, 2), [4, 4, 0, 0]); ctx2.fill(); ctx2.fillStyle = '#666'; ctx2.font = '10px Inter, sans-serif'; ctx2.textAlign = 'center'; ctx2.fillText(dayLabels[idx], x + barW / 2, H - 8); }); } // Session Tally Emoji System const tallyContainer = document.getElementById('sessionTallyContainer'); let currentTallyStyle = localStorage.getItem('fliqlow_tally_style') || 'tomatoes'; const customEmojiSetsRaw = JSON.parse(localStorage.getItem('fliqlow_custom_emojis')) || {}; const customEmojiSets = {}; Object.keys(customEmojiSetsRaw).forEach(k => { if (Array.isArray(customEmojiSetsRaw[k])) { customEmojiSets[k] = { name: 'Custom', emojis: customEmojiSetsRaw[k] }; } else { customEmojiSets[k] = customEmojiSetsRaw[k]; } }); const tallyDictionaries = { tomatoes: ['🍅', '🍅', '🍅', '🍅'], plants: ['ðŸŒą', 'ðŸŒŋ', 'ðŸŠī', 'ðŸŒē'], dots: ['⚩', '⚩', '⚩', '⚩'], hearts: ['💖', '💝', '💗', '💓'], stars: ['⭐', '🌟', 'âœĻ', 'ðŸ’Ŧ'], space: ['🚀', 'ðŸ›ļ', '🛰ïļ', '🊐'], fire: ['ðŸ”Ĩ', 'ðŸ”Ĩ', 'ðŸ”Ĩ', 'ðŸ”Ĩ'], brain: ['🧠', '🧠', '🧠', '🧠'], hamster: ['ðŸđ', 'ðŸđ', 'ðŸđ', 'ðŸđ'], book: ['📚', '📚', '📚', '📚'], coffee: ['☕', '☕', '☕', '☕'], wave: ['🌊', '🌊', '🌊', '🌊'] }; Object.keys(customEmojiSets).forEach(k => { tallyDictionaries[k] = customEmojiSets[k].emojis; }); const emojiPickerToggle = document.getElementById('emojiPickerToggle'); const emojiPickerPopup = document.getElementById('emojiPickerPopup'); const emojiOpts = document.querySelectorAll('.emoji-opt'); if (emojiPickerToggle && emojiPickerPopup) { emojiPickerToggle.addEventListener('click', () => { emojiPickerPopup.classList.toggle('hidden'); }); // Close on outside click document.addEventListener('click', (e) => { if (!e.target.closest('#sessionTallyContainer') && !e.target.closest('#emojiPickerPopup')) { emojiPickerPopup.classList.add('hidden'); } }); } function updateTallyStyle(style, customEmojis = null) { currentTallyStyle = style; localStorage.setItem('fliqlow_tally_style', currentTallyStyle); if (customEmojis) { tallyDictionaries[style] = Array.isArray(customEmojis) ? customEmojis : Array.from(new Intl.Segmenter(navigator.language, { granularity: 'grapheme' }).segment(customEmojis)).map(x => x.segment); } // Update active class on static options document.querySelectorAll('.emoji-opt').forEach(o => o.classList.toggle('active', o.dataset.style === currentTallyStyle)); // Update active class on custom sets document.querySelectorAll('.custom-tally-item').forEach(o => { if (o.dataset.style === currentTallyStyle) { o.style.borderColor = '#a855f7'; o.style.background = 'rgba(168, 85, 247, 0.2)'; } else { o.style.borderColor = 'rgba(255,255,255,0.1)'; o.style.background = 'rgba(255,255,255,0.05)'; } }); if (emojiPickerPopup) emojiPickerPopup.classList.add('hidden'); renderSessionTally(); } document.querySelectorAll('.emoji-opt').forEach(opt => { opt.addEventListener('click', () => { updateTallyStyle(opt.dataset.style); }); }); const saveCustomEmojiBtn = document.getElementById('saveCustomEmojiBtn'); const customEmojiInput = document.getElementById('customEmojiInput'); const customEmojiNameInput = document.getElementById('customEmojiNameInput'); const customTallySetsContainer = document.getElementById('customTallySetsContainer'); function renderCustomTallySets() { if (!customTallySetsContainer) return; customTallySetsContainer.innerHTML = ''; getAllTallyEmojiSets().then(sets => { sets.sort((a, b) => b.createdAt - a.createdAt); sets.forEach(set => { const el = document.createElement('div'); el.className = 'custom-tally-set-item'; el.style.display = 'flex'; el.style.justifyContent = 'space-between'; el.style.alignItems = 'center'; el.style.padding = '8px'; el.style.marginBottom = '6px'; el.style.borderRadius = '6px'; el.style.border = '1px solid rgba(255,255,255,0.1)'; el.style.background = 'rgba(255,255,255,0.05)'; if (currentTallyStyle === set.id) { el.style.borderColor = '#a855f7'; el.style.background = 'rgba(168, 85, 247, 0.2)'; } el.innerHTML = `
${set.name}
${set.emojis}
`; el.querySelector('.tally-set-info').addEventListener('click', () => { updateTallyStyle(set.id, set.emojis); }); el.querySelector('.delete-tally-btn').addEventListener('click', (e) => { e.stopPropagation(); deleteTallyEmojiSet(set.id).then(renderCustomTallySets); }); customTallySetsContainer.appendChild(el); }); }); } if (saveCustomEmojiBtn) { saveCustomEmojiBtn.addEventListener('click', () => { if (!customEmojiInput) return; const emojis = customEmojiInput.value.trim(); const name = (customEmojiNameInput && customEmojiNameInput.value.trim()) || 'Custom Set'; if (!emojis) return; const setObj = { id: 'tally_' + Date.now(), name: name, emojis: emojis, createdAt: Date.now() }; saveTallyEmojiSet(setObj).then(() => { customEmojiInput.value = ''; if (customEmojiNameInput) customEmojiNameInput.value = ''; renderCustomTallySets(); updateTallyStyle(setObj.id, setObj.emojis); }); }); } function initCustomEmojisUI() { renderCustomTallySets(); } initCustomEmojisUI(); (function initTallyStyle() { emojiOpts.forEach(o => o.classList.toggle('active', o.dataset.style === currentTallyStyle)); renderSessionTally(); })(); function renderSessionTally() { if (!tallyContainer) return; const count = focusStats.completedFocusSessions || 0; tallyContainer.style.display = 'flex'; // Check if we need to insert the wrapper inside the container dynamically let wrapper = document.getElementById('tallyEmojiWrapper'); if (!wrapper) { // Find badge and toggle const badge = document.getElementById('sessionCounterBadge'); const toggle = document.getElementById('emojiPickerToggle'); tallyContainer.innerHTML = ''; wrapper = document.createElement('div'); wrapper.id = 'tallyEmojiWrapper'; wrapper.style.display = 'flex'; wrapper.style.gap = '15px'; wrapper.style.alignItems = 'center'; tallyContainer.appendChild(wrapper); if (badge) tallyContainer.appendChild(badge); if (toggle) tallyContainer.appendChild(toggle); } const badge = document.getElementById('sessionCounterBadge'); if (badge) badge.textContent = count; wrapper.innerHTML = ''; const dict = tallyDictionaries[currentTallyStyle] || tallyDictionaries['tomatoes']; // Use the set's emoji count as max slots const MAX_SLOTS = dict.length; const cycleProgress = count % MAX_SLOTS; // How many to show as active: if count > 0 and cycleProgress === 0, show all as briefly completed then reset const activeCount = (count > 0 && cycleProgress === 0) ? 0 : cycleProgress; for (let i = 0; i < MAX_SLOTS; i++) { const span = document.createElement('span'); span.className = 'tally-icon'; const emojiToUse = dict[i % dict.length]; span.textContent = emojiToUse; if (i < activeCount) { span.classList.add('active'); span.style.opacity = '1'; span.style.filter = 'drop-shadow(0 0 6px rgba(255, 255, 255, 0.4))'; } else { span.classList.add('faded'); span.style.opacity = '0.2'; } wrapper.appendChild(span); } } // ========================================== // YOUTUBE PIP WIDGET // ========================================== const youtubeWidget = document.getElementById('youtubeWidget'); const youtubeToggle = document.getElementById('youtubeToggle'); const ytCloseBtn = document.getElementById('ytCloseBtn'); const ytMinimizeBtn = document.getElementById('ytMinimizeBtn'); const loadYoutubeBtn = document.getElementById('loadYoutubeBtn'); const youtubeLink = document.getElementById('youtubeLink'); const ytDragHandle = document.getElementById('ytDragHandle'); if (youtubeToggle && youtubeWidget) { youtubeToggle.addEventListener('click', () => { youtubeWidget.classList.toggle('hidden'); }); } if (ytCloseBtn && youtubeWidget) { ytCloseBtn.addEventListener('click', () => { youtubeWidget.classList.add('hidden'); youtubeWidget.classList.remove('minimized'); }); } if (ytMinimizeBtn && youtubeWidget) { ytMinimizeBtn.addEventListener('click', () => { youtubeWidget.classList.toggle('minimized'); const icon = ytMinimizeBtn.querySelector('i'); if (youtubeWidget.classList.contains('minimized')) { icon.className = 'fas fa-expand'; } else { icon.className = 'fas fa-minus'; } }); } function parseYoutubeUrl(url) { if (!url) return null; // Support various YouTube URL formats let videoId = null; // youtu.be/ID const shortMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]+)/); if (shortMatch) videoId = shortMatch[1]; // youtube.com/watch?v=ID const watchMatch = url.match(/[?&]v=([a-zA-Z0-9_-]+)/); if (watchMatch) videoId = watchMatch[1]; // youtube.com/embed/ID const embedMatch = url.match(/youtube\.com\/embed\/([a-zA-Z0-9_-]+)/); if (embedMatch) videoId = embedMatch[1]; // youtube.com/shorts/ID const shortsMatch = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]+)/); if (shortsMatch) videoId = shortsMatch[1]; if (videoId) { return `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`; } return null; } const ytFullscreenBtn = document.getElementById('ytFullscreenBtn'); const ytChangeLinkDiv = document.getElementById('ytChangeLinkDiv'); const ytInputGroup = document.getElementById('ytInputGroup'); const ytChangeLinkBtn = document.getElementById('ytChangeLinkBtn'); if (ytFullscreenBtn) { ytFullscreenBtn.addEventListener('click', () => { youtubeWidget.classList.toggle('fullscreen-mode'); const icon = ytFullscreenBtn.querySelector('i'); if (youtubeWidget.classList.contains('fullscreen-mode')) { icon.className = 'fas fa-compress'; } else { icon.className = 'fas fa-expand'; } }); } if (ytChangeLinkBtn) { ytChangeLinkBtn.addEventListener('click', () => { ytInputGroup.classList.remove('hidden'); ytChangeLinkDiv.classList.add('hidden'); }); } if (loadYoutubeBtn) { loadYoutubeBtn.addEventListener('click', () => { const rawUrl = youtubeLink.value.trim(); if (!rawUrl) return; const embedUrl = parseYoutubeUrl(rawUrl); if (embedUrl) { const playerContainer = document.getElementById('youtubePlayerContainer'); playerContainer.innerHTML = ``; if (ytInputGroup && ytChangeLinkDiv) { ytInputGroup.classList.add('hidden'); ytChangeLinkDiv.classList.remove('hidden'); } } }); } // YouTube PiP Drag if (ytDragHandle && youtubeWidget) { let ytDragging = false; let ytDragStartX = 0; let ytDragStartY = 0; let ytStartLeft = 20; let ytStartBottom = 20; ytDragHandle.addEventListener('mousedown', (e) => { if (e.target.closest('.yt-pip-btn')) return; ytDragging = true; ytDragStartX = e.clientX; ytDragStartY = e.clientY; const rect = youtubeWidget.getBoundingClientRect(); ytStartLeft = rect.left; ytStartBottom = window.innerHeight - rect.bottom; youtubeWidget.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!ytDragging) return; const deltaX = e.clientX - ytDragStartX; const deltaY = e.clientY - ytDragStartY; let newLeft = ytStartLeft + deltaX; let newBottom = ytStartBottom - deltaY; // Clamp newLeft = Math.max(0, Math.min(window.innerWidth - 200, newLeft)); newBottom = Math.max(0, Math.min(window.innerHeight - 100, newBottom)); youtubeWidget.style.left = newLeft + 'px'; youtubeWidget.style.bottom = newBottom + 'px'; }); document.addEventListener('mouseup', () => { if (ytDragging) { ytDragging = false; youtubeWidget.style.transition = ''; } }); // Touch drag support ytDragHandle.addEventListener('touchstart', (e) => { if (e.target.closest('.yt-pip-btn') || e.touches.length !== 1) return; ytDragging = true; ytDragStartX = e.touches[0].clientX; ytDragStartY = e.touches[0].clientY; const rect = youtubeWidget.getBoundingClientRect(); ytStartLeft = rect.left; ytStartBottom = window.innerHeight - rect.bottom; youtubeWidget.style.transition = 'none'; }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!ytDragging || e.touches.length !== 1) return; const deltaX = e.touches[0].clientX - ytDragStartX; const deltaY = e.touches[0].clientY - ytDragStartY; let newLeft = ytStartLeft + deltaX; let newBottom = ytStartBottom - deltaY; newLeft = Math.max(0, Math.min(window.innerWidth - 200, newLeft)); newBottom = Math.max(0, Math.min(window.innerHeight - 100, newBottom)); youtubeWidget.style.left = newLeft + 'px'; youtubeWidget.style.bottom = newBottom + 'px'; }, { passive: true }); document.addEventListener('touchend', () => { if (ytDragging) { ytDragging = false; youtubeWidget.style.transition = ''; } }); } // ========================================== // PET TRACKER RESIZE HANDLE // ========================================== const petResizeHandle = document.getElementById('petResizeHandle'); const taskTrackerWrapper = document.querySelector('.task-tracker-wrapper'); if (petResizeHandle && taskTrackerWrapper) { let isResizing = false; let resizeStartX = 0; let resizeStartY = 0; let resizeStartScale = 1; function getCurrentScale() { const val = getComputedStyle(document.documentElement).getPropertyValue('--tracker-scale'); return parseFloat(val) || 1; } } // ========================================== // PET TRACKER RESIZE HANDLE // ========================================== if (petResizeHandle && taskTrackerWrapper) { let isResizing = false; let resizeStartX = 0; let resizeStartY = 0; let resizeStartScale = 1; function getCurrentScale() { const val = getComputedStyle(document.documentElement).getPropertyValue('--tracker-scale'); return parseFloat(val) || 1; } petResizeHandle.addEventListener('mousedown', (e) => { isResizing = true; resizeStartX = e.clientX; resizeStartY = e.clientY; resizeStartScale = getCurrentScale(); e.preventDefault(); e.stopPropagation(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; // Diagonal drag: bottom-left is resize direction const deltaX = resizeStartX - e.clientX; const deltaY = e.clientY - resizeStartY; const delta = (deltaX + deltaY) / 200; let newScale = resizeStartScale + delta; newScale = Math.max(0.5, Math.min(1.5, newScale)); document.documentElement.style.setProperty('--tracker-scale', newScale); }); document.addEventListener('mouseup', () => { isResizing = false; }); // Touch support petResizeHandle.addEventListener('touchstart', (e) => { if (e.touches.length !== 1) return; isResizing = true; resizeStartX = e.touches[0].clientX; resizeStartY = e.touches[0].clientY; resizeStartScale = getCurrentScale(); e.stopPropagation(); }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!isResizing || e.touches.length !== 1) return; const deltaX = resizeStartX - e.touches[0].clientX; const deltaY = e.touches[0].clientY - resizeStartY; const delta = (deltaX + deltaY) / 200; let newScale = resizeStartScale + delta; newScale = Math.max(0.5, Math.min(1.5, newScale)); document.documentElement.style.setProperty('--tracker-scale', newScale); }, { passive: true }); document.addEventListener('touchend', () => { isResizing = false; }); // Double-click to toggle expand/collapse taskTrackerWrapper.addEventListener('dblclick', (e) => { if (e.target.closest('button') || e.target.closest('input')) return; const currentScale = getCurrentScale(); const newScale = currentScale < 1 ? 1 : 0.6; document.documentElement.style.setProperty('--tracker-scale', newScale); }); } // end if (petResizeHandle && taskTrackerWrapper) // ========================================== // GLOBAL ESCAPE KEY HANDLER // ========================================== document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { // Close stats const sm = document.getElementById('statsModal'); if (sm && !sm.classList.contains('hidden')) { sm.classList.add('hidden'); return; } // Close eventEditorModal (Notes) const eem = document.getElementById('eventEditorModal'); if (eem && !eem.classList.contains('hidden')) { eem.classList.add('hidden'); return; } // Close calendar const calO = document.getElementById('calendarOverlay'); if (calO && !calO.classList.contains('hidden')) { calO.classList.add('hidden'); return; } // Close taskPanel const taskP = document.getElementById('taskPanel'); if (taskP && !taskP.classList.contains('hidden')) { taskP.classList.add('hidden'); return; } // Close settings const setM = document.getElementById('settingsModal'); if (setM && !setM.classList.contains('hidden')) { setM.classList.add('hidden'); return; } // Close scene panel const sp = document.getElementById('scenePanel'); if (sp && !sp.classList.contains('hidden')) { sp.classList.add('hidden'); return; } // Close alert drawer const ad = document.getElementById('alertDrawer'); if (ad && !ad.classList.contains('hidden')) { ad.classList.add('hidden'); return; } // Close YouTube fullscreen const yw = document.getElementById('youtubeWidget'); if (yw && yw.classList.contains('fullscreen-mode')) { yw.classList.remove('fullscreen-mode'); return; } } }); // Close alert drawer on outside click document.addEventListener('click', (e) => { const alertDrawer = document.getElementById('alertDrawer'); const alertPanel = document.getElementById('alertPanel'); if (alertDrawer && !alertDrawer.classList.contains('hidden')) { if (!e.target.closest('#alertPanel')) { alertDrawer.classList.add('hidden'); } } }); // Close stats modal on outside click (click on the modal backdrop) document.addEventListener('click', (e) => { const sm = document.getElementById('statsModal'); if (sm && !sm.classList.contains('hidden')) { if (e.target === sm) { sm.classList.add('hidden'); } } }); // ========================================== // PAGE NAVIGATION SYSTEM // ========================================== const pages = document.querySelectorAll('.app-page'); const navItems = document.querySelectorAll('.nav-item'); const startFocusBtn = document.getElementById('startFocusBtn'); function switchPage(pageId) { pages.forEach(page => { if (page.id === pageId + 'Page') { page.classList.remove('hidden'); } else { page.classList.add('hidden'); } }); navItems.forEach(item => { if (item.getAttribute('data-page') === pageId) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // Logo visibility or other global adjustments const logo = document.getElementById('mainLogo'); if (logo) { if (pageId === 'focus') { logo.style.opacity = '0.5'; } else { logo.style.opacity = '1'; } } localStorage.setItem('fliqlow_last_page', pageId); } if (navItems) { navItems.forEach(item => { item.addEventListener('click', () => { switchPage(item.getAttribute('data-page')); }); }); } if (startFocusBtn) { startFocusBtn.addEventListener('click', () => switchPage('focus')); } // Restore last page const lastPage = localStorage.getItem('fliqlow_last_page') || 'home'; switchPage(lastPage); // ========================================== // REALTIME FLIP CLOCK LOGIC // ========================================== function createFlipClockHTML(containerId) { const container = document.getElementById(containerId); if (!container) return; container.innerHTML = `
0
0
0
0
0
0
0
0
:
0
0
0
0
0
0
0
0
`; } function updateRealtimeClock(containerId) { const now = new Date(); const hours = now.getHours(); const minutes = now.getMinutes(); updateRealtimeSection(containerId + '_hours', hours); updateRealtimeSection(containerId + '_minutes', minutes); } function updateRealtimeSection(sectionId, value) { const section = document.getElementById(sectionId); if (!section) return; const str = value.toString().padStart(2, '0'); const segments = section.querySelectorAll('.time-segment'); for (let i = 0; i < str.length; i++) { const val = parseInt(str[i]); const segment = segments[i]; const top = segment.querySelector('.segment-display__top'); const bottom = segment.querySelector('.segment-display__bottom'); const overTop = segment.querySelector('.segment-overlay__top'); const overBottom = segment.querySelector('.segment-overlay__bottom'); const overlay = segment.querySelector('.segment-overlay'); if (top.textContent !== str[i]) { overlay.classList.add('flip'); top.textContent = str[i]; overBottom.textContent = str[i]; const finish = () => { overlay.classList.remove('flip'); bottom.textContent = str[i]; overTop.textContent = str[i]; overlay.removeEventListener('animationend', finish); }; overlay.addEventListener('animationend', finish); } } } // Init realtime clocks createFlipClockHTML('realtimeClockHome'); createFlipClockHTML('realtimeClockFocus'); setInterval(() => { updateRealtimeClock('realtimeClockHome'); updateRealtimeClock('realtimeClockFocus'); }, 1000); // ========================================== // MORE MENU LOGIC // ========================================== const moreMenuToggle = document.getElementById('moreMenuToggle'); const moreMenuDrawer = document.getElementById('moreMenuDrawer'); const realtimeClockToggle = document.getElementById('realtimeClockToggle'); const focusRealtimeClock = document.getElementById('focusRealtimeClock'); if (moreMenuToggle) { moreMenuToggle.addEventListener('click', (e) => { e.stopPropagation(); if (moreMenuDrawer) moreMenuDrawer.classList.toggle('hidden'); }); } document.addEventListener('click', (e) => { if (moreMenuDrawer && !moreMenuDrawer.classList.contains('hidden') && !e.target.closest('#moreMenuToggle')) { moreMenuDrawer.classList.add('hidden'); } }); if (realtimeClockToggle) { realtimeClockToggle.addEventListener('click', () => { if (focusRealtimeClock) { focusRealtimeClock.classList.toggle('hidden'); localStorage.setItem('fliqlow_realtime_clock_visible', !focusRealtimeClock.classList.contains('hidden')); } }); } // Restore realtime clock visibility if (localStorage.getItem('fliqlow_realtime_clock_visible') === 'true') { if (focusRealtimeClock) focusRealtimeClock.classList.remove('hidden'); } // Close button on the floating realtime clock const focusClockClose = document.getElementById('focusClockClose'); if (focusClockClose && focusRealtimeClock) { focusClockClose.addEventListener('click', (e) => { e.stopPropagation(); focusRealtimeClock.classList.add('hidden'); localStorage.setItem('fliqlow_realtime_clock_visible', 'false'); }); } // ========================================== // HOME PAGE MOTIVATION // ========================================== const HOME_QUOTES = [ "\"Time to recharge and reset.\"", "\"I hope the world is kind to you.\"", "\"One step at a time.\"", "\"You are allowed to begin again.\"", "\"Focus gently.\"", "\"Deep work. Soft life.\"" ]; const homeQuoteEl = document.getElementById('homeQuote'); if (homeQuoteEl) { let quoteIdx = 0; setInterval(() => { quoteIdx = (quoteIdx + 1) % HOME_QUOTES.length; homeQuoteEl.style.opacity = '0'; setTimeout(() => { homeQuoteEl.textContent = HOME_QUOTES[quoteIdx]; homeQuoteEl.style.opacity = '1'; }, 1000); }, 15000); } // ========================================== // TALLY EMOJI SAVING SYSTEM // ========================================== const TALLY_STORE = 'saved_tally_emojis'; function initTallyDB() { return new Promise((resolve, reject) => { const DB_NAME = 'fliqlow_db'; const DB_VERSION = 2; const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains(TALLY_STORE)) { db.createObjectStore(TALLY_STORE, { keyPath: 'id' }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } function saveTallyEmojiSet(setObj) { return initTallyDB().then(db => { return new Promise((resolve, reject) => { const tx = db.transaction(TALLY_STORE, 'readwrite'); tx.objectStore(TALLY_STORE).put(setObj); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); }); } function getAllTallyEmojiSets() { return initTallyDB().then(db => { const tx = db.transaction(TALLY_STORE, 'readonly'); const req = tx.objectStore(TALLY_STORE).getAll(); return new Promise(resolve => { req.onsuccess = () => resolve(req.result || []); }); }); } function deleteTallyEmojiSet(id) { return initTallyDB().then(db => { return new Promise((resolve, reject) => { const tx = db.transaction(TALLY_STORE, 'readwrite'); tx.objectStore(TALLY_STORE).delete(id); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); }); } // Initial render renderCustomTallySets(); // ========================================== // LOCAL TIME WIDGET LOGIC // ========================================== const localTimeWidget = document.getElementById('localTimeWidget'); const localTimeToggle = document.getElementById('localTimeToggle'); const closeLocalTime = document.getElementById('closeLocalTime'); const collapseLocalTime = document.getElementById('collapseLocalTime'); const localTimeDisplay = document.getElementById('localTimeDisplay'); const localDateDisplay = document.getElementById('localDateDisplay'); function updateLocalTime() { const now = new Date(); const timeStr = now.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const dateStr = now.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' }); if (localTimeDisplay) localTimeDisplay.textContent = timeStr; if (localDateDisplay) localDateDisplay.textContent = dateStr; } if (localTimeToggle) { localTimeToggle.addEventListener('click', () => { if (localTimeWidget) { localTimeWidget.classList.toggle('hidden'); if (!localTimeWidget.classList.contains('hidden')) { updateLocalTime(); } } }); } if (closeLocalTime) { closeLocalTime.addEventListener('click', () => { if (localTimeWidget) localTimeWidget.classList.add('hidden'); }); } if (collapseLocalTime) { collapseLocalTime.addEventListener('click', () => { if (localTimeWidget) { localTimeWidget.classList.toggle('collapsed'); const icon = collapseLocalTime.querySelector('i'); if (localTimeWidget.classList.contains('collapsed')) { icon.className = 'fas fa-chevron-up'; } else { icon.className = 'fas fa-chevron-down'; } } }); } setInterval(updateLocalTime, 1000); updateLocalTime(); // Generic Draggable Logic for Floating Widgets function makeDraggable(el, header) { if (!el || !header) return; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = dragMouseDown; header.ontouchstart = dragTouchStart; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function dragTouchStart(e) { if (e.touches.length !== 1) return; pos3 = e.touches[0].clientX; pos4 = e.touches[0].clientY; document.ontouchend = closeDragElement; document.ontouchmove = elementTouchDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; el.style.top = (el.offsetTop - pos2) + "px"; el.style.left = (el.offsetLeft - pos1) + "px"; } function elementTouchDrag(e) { if (e.touches.length !== 1) return; pos1 = pos3 - e.touches[0].clientX; pos2 = pos4 - e.touches[0].clientY; pos3 = e.touches[0].clientX; pos4 = e.touches[0].clientY; el.style.top = (el.offsetTop - pos2) + "px"; el.style.left = (el.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; document.ontouchend = null; document.ontouchmove = null; } } if (localTimeWidget) { const header = localTimeWidget.querySelector('.draggable-header'); if (header) makeDraggable(localTimeWidget, header); } // ========================================== // AMBIENCE TOGGLE LOGIC // ========================================== const ambienceToggleBtn = document.getElementById('ambienceToggleBtn'); const ambienceDrawer = document.getElementById('ambienceDrawer'); if (ambienceToggleBtn && ambienceDrawer) { ambienceToggleBtn.addEventListener('click', (e) => { e.stopPropagation(); ambienceDrawer.classList.toggle('hidden'); ambienceToggleBtn.classList.toggle('active', !ambienceDrawer.classList.contains('hidden')); }); document.addEventListener('click', (e) => { if (!ambienceDrawer.classList.contains('hidden') && !e.target.closest('#ambienceDrawer') && !e.target.closest('#ambienceToggleBtn')) { ambienceDrawer.classList.add('hidden'); ambienceToggleBtn.classList.remove('active'); } }); } // ========================================== // FOCUS WORLD COMPANION SYSTEM // ========================================== const _COMPANION_FPS = 10; const _COMPANION_HEIGHT_PX = 78; // prefix: filename prefix inside the folder (empty string = just the number) // e.g. folder='knightmovement/', prefix='knightwalk', start=49 → 'knightmovement/knightwalk0049.png' const _COMPANIONS = [ { id: 'cat', name: 'Lily', bg: 'catbge.png', folder: 'catwalk/', prefix: '', start: 46, end: 63 }, { id: 'girl', name: 'FeiFei', bg: 'girlbge.png', folder: 'girlwalking/', prefix: 'girlwalking', start: 1, end: 45 }, { id: 'hamster', name: 'Bigbike', bg: 'hamsterbge.png', folder: 'hamsterwalk/', prefix: 'hamsterwalk', start: 48, end: 73 }, { id: 'knight', name: 'Phu', bg: 'knightbge.png', folder: 'knightmovement/', prefix: 'knightwalk', start: 49, end: 78 }, { id: 'traveler', name: 'John', bg: 'travelerbge.png', folder: 'travelermovement/', prefix: 'travelerwalk', start: 48, end: 78 }, { id: 'turtle', name: 'Heng&Dee', bg: 'turtlebge.png', folder: 'turtlewalking/', prefix: 'turtlewalking',start: 3, end: 47 }, { id: 'dog', name: 'Poppy', bg: 'dogbge.png', folder: 'dogwalk/', prefix: 'dogwalk', start: 1, end: 121 }, ]; let _companionFrames = []; let _companionFrameIdx = 0; let _companionIntervalId = null; let _companionPosX = 0; let _companionDir = 1; const _companionSpeed = 1.2; let _activeCompanionIdx = (function() { const v = parseInt(localStorage.getItem('fliqlow_companion_idx'), 10); return (isNaN(v) || v < 0 || v >= _COMPANIONS.length) ? 1 : v; // 1 = FeiFei default })(); function _saveCompanionIdx() { localStorage.setItem('fliqlow_companion_idx', String(_activeCompanionIdx)); } // Per-companion frame cache — loaded once, reused instantly on every switch const _companionCache = {}; // Low-level: load an array of image URLs in parallel, return the ones that succeed function _loadImages(urls) { return Promise.all(urls.map(function(src) { return new Promise(function(resolve) { var img = new Image(); img.onload = function() { resolve(src); }; img.onerror = function() { resolve(null); }; img.src = src; }); })).then(function(r) { return r.filter(Boolean); }); } // Build ordered URL list for a companion function _buildCompanionUrls(companion) { var urls = []; for (var i = companion.start; i <= companion.end; i++) { urls.push(companion.folder + (companion.prefix || '') + String(i).padStart(4, '0') + '.png'); } return urls; } // Full load (used by background preloader) — always resolves complete set async function _loadCompanionFrames(companion) { if (_companionCache[companion.id] && _companionCache[companion.id].length > 0) { return _companionCache[companion.id]; } const frames = await _loadImages(_buildCompanionUrls(companion)); _companionCache[companion.id] = frames; return frames; } // Progressive switch: show companion after first QUICK frames, grow array in background. // The animation loop uses `idx % frames.length` so extra frames slot in seamlessly. const _QUICK_FRAMES = 6; async function _switchCompanionProgressive(companion) { // Already fully cached → instant if (_companionCache[companion.id] && _companionCache[companion.id].length > 0) { _companionFrames = _companionCache[companion.id]; return; } const urls = _buildCompanionUrls(companion); const quickUrls = urls.slice(0, _QUICK_FRAMES); const restUrls = urls.slice(_QUICK_FRAMES); // Wait only for the quick batch (FeiFei's first frames are preloaded → near-instant) const quickFrames = await _loadImages(quickUrls); if (quickFrames.length === 0) { // Fallback: full load if quick batch failed const all = await _loadImages(urls); _companionCache[companion.id] = all; _companionFrames = all; return; } // Show now with quick frames _companionFrames = quickFrames; // live array — will grow below // Load rest in background and push into the SAME array the animation loop is using if (restUrls.length > 0) { var liveArray = _companionFrames; _loadImages(restUrls).then(function(rest) { rest.forEach(function(src) { liveArray.push(src); }); // Only cache when we're still on this companion if (_companionFrames === liveArray) { _companionCache[companion.id] = liveArray.slice(); } }); } else { _companionCache[companion.id] = quickFrames.slice(); } } // Kick off bg-image preload immediately (tiny files, makes picker look instant) function _preloadBgImages() { _COMPANIONS.forEach(function(c) { var img = new Image(); img.src = c.bg; }); } // After a companion finishes loading, update its skeleton card in the picker if open function _refreshPickerCard(companionId, bgSrc) { var btn = document.querySelector('.companion-pick-btn[data-cid="' + companionId + '"]'); if (!btn) return; btn.classList.remove('cpb-loading'); btn.style.backgroundImage = 'url(' + bgSrc + ')'; } // Preload every companion sequentially in background; update picker cards live function _preloadAllInBackground() { var i = 0; function next() { if (i >= _COMPANIONS.length) return; var c = _COMPANIONS[i++]; if (_companionCache[c.id] && _companionCache[c.id].length > 0) { _refreshPickerCard(c.id, c.bg); // already done — just make sure card is shown next(); return; } _loadCompanionFrames(c).then(function() { _refreshPickerCard(c.id, c.bg); setTimeout(next, 200); }); } next(); // start immediately — no extra delay } function _startCompanionAnimation() { if (_companionIntervalId) return; const spriteEl = document.getElementById('catCompanion'); if (!spriteEl || _companionFrames.length === 0) return; _companionIntervalId = setInterval(function() { _companionFrameIdx = (_companionFrameIdx + 1) % _companionFrames.length; spriteEl.src = _companionFrames[_companionFrameIdx]; const card = document.querySelector('.focus-world-card'); const maxX = card ? card.clientWidth - _COMPANION_HEIGHT_PX - 10 : 440; _companionPosX += _companionSpeed * _companionDir; if (_companionPosX >= maxX) { _companionPosX = maxX; _companionDir = -1; spriteEl.style.transform = 'scaleX(-1)'; } else if (_companionPosX <= 0) { _companionPosX = 0; _companionDir = 1; spriteEl.style.transform = 'scaleX(1)'; } spriteEl.style.left = _companionPosX + 'px'; }, 1000 / _COMPANION_FPS); } function _pauseCompanionAnimation() { if (_companionIntervalId) { clearInterval(_companionIntervalId); _companionIntervalId = null; } } async function _switchCompanion(idx) { _activeCompanionIdx = idx; _saveCompanionIdx(); const companion = _COMPANIONS[idx]; const bgEl = document.getElementById('focusWorldBg'); const nameEl = document.getElementById('companionName'); const spriteEl = document.getElementById('catCompanion'); const card = document.querySelector('.focus-world-card'); if (bgEl) bgEl.src = companion.bg; if (nameEl) nameEl.textContent = companion.name; _pauseCompanionAnimation(); _companionFrames = []; _companionFrameIdx = 0; _companionPosX = 0; _companionDir = 1; // Hide sprite + shimmer while first batch downloads if (spriteEl) { spriteEl.style.transition = 'none'; spriteEl.style.opacity = '0'; spriteEl.style.left = '0px'; spriteEl.style.transform = 'scaleX(1)'; } if (card) card.classList.add('fw-companion-loading'); // Progressive load: shows companion after just the first QUICK frames, // then silently pushes the rest into the live array while animating await _switchCompanionProgressive(companion); if (_companionFrames.length === 0) { _companionFrames = [companion.folder + (companion.prefix || '') + String(companion.start).padStart(4, '0') + '.png']; } // Frames ready — fade in if (spriteEl && _companionFrames.length > 0) { spriteEl.src = _companionFrames[0]; spriteEl.style.display = 'block'; spriteEl.dataset.companion = companion.id; requestAnimationFrame(function() { spriteEl.style.transition = 'opacity 0.3s ease'; spriteEl.style.opacity = '1'; setTimeout(function() { spriteEl.style.transition = 'none'; }, 350); }); } if (card) card.classList.remove('fw-companion-loading'); // Only walk if timer is already running; otherwise stay idle until Start is pressed if (typeof isRunning !== 'undefined' && isRunning) { _startCompanionAnimation(); } } // Override setAnimalAnimationPaused so existing timer code drives companion animation function setAnimalAnimationPaused(isPaused) { if (isPaused) { _pauseCompanionAnimation(); } else { _startCompanionAnimation(); } const circleHamster = document.getElementById('circleHamster'); if (circleHamster) { circleHamster.classList.toggle('paused', !!isPaused); } } // --- Companion Picker --- function _buildCompanionPicker() { const picker = document.getElementById('companionPicker'); if (!picker) return; picker.innerHTML = ''; const grid = document.createElement('div'); grid.className = 'companion-picker-grid'; _COMPANIONS.forEach(function(c, i) { const cached = _companionCache[c.id] && _companionCache[c.id].length > 0; const btn = document.createElement('button'); btn.className = 'companion-pick-btn' + (i === _activeCompanionIdx ? ' active' : '') + (cached ? '' : ' cpb-loading'); btn.dataset.cid = c.id; btn.title = c.name; if (cached) { btn.style.backgroundImage = 'url(' + c.bg + ')'; } const label = document.createElement('span'); label.className = 'cpb-name'; label.textContent = c.name; btn.appendChild(label); if (!cached) { const spinner = document.createElement('span'); spinner.className = 'cpb-spinner'; btn.appendChild(spinner); } btn.addEventListener('click', function() { _switchCompanion(i); _closePicker(); }); grid.appendChild(btn); }); picker.appendChild(grid); } function _openPicker() { const picker = document.getElementById('companionPicker'); if (!picker) return; const btn = document.getElementById('changeCompanionBtn'); if (btn) { const rect = btn.getBoundingClientRect(); picker.style.left = Math.max(10, rect.left - 80) + 'px'; picker.style.bottom = (window.innerHeight - rect.top + 10) + 'px'; picker.style.top = 'auto'; } _buildCompanionPicker(); picker.classList.remove('hidden'); } function _closePicker() { const picker = document.getElementById('companionPicker'); if (picker) picker.classList.add('hidden'); } (function() { const changeBtn = document.getElementById('changeCompanionBtn'); if (changeBtn) { function _togglePicker(e) { e.stopPropagation(); e.preventDefault(); const picker = document.getElementById('companionPicker'); if (picker && picker.classList.contains('hidden')) { _openPicker(); } else { _closePicker(); } } changeBtn.addEventListener('click', _togglePicker); changeBtn.addEventListener('touchend', function(e) { // touchend → synthetic click also fires; handle here and cancel the click if (e.cancelable) e.preventDefault(); _togglePicker(e); }, { passive: false }); } document.addEventListener('click', function(e) { if (!e.target.closest('#companionPicker') && !e.target.closest('#changeCompanionBtn')) { _closePicker(); } }); })(); // --- Focus Level / XP system --- const _FOCUS_XP_PER_SESSION = 120; function _getFocusMaxXP(level) { return Math.round(500 * Math.pow(1.3, level - 1)); } let _focusLevel = (function() { const v = parseInt(localStorage.getItem('fliqlow_focus_level'), 10); return (v && v > 0) ? v : 1; })(); let _focusXP = (function() { const v = parseInt(localStorage.getItem('fliqlow_focus_xp'), 10); return (v && v >= 0) ? v : 0; })(); function _saveFocusData() { localStorage.setItem('fliqlow_focus_level', String(_focusLevel)); localStorage.setItem('fliqlow_focus_xp', String(_focusXP)); } function _renderFocusLevelUI() { const maxXP = _getFocusMaxXP(_focusLevel); const elLv = document.getElementById('focusLevel'); const elCur = document.getElementById('currentXP'); const elMax = document.getElementById('maxXP'); const elFill = document.getElementById('xpFill'); if (elLv) elLv.textContent = _focusLevel; if (elCur) elCur.textContent = _focusXP; if (elMax) elMax.textContent = maxXP; if (elFill) { const pct = Math.min(100, (_focusXP / maxXP) * 100); elFill.style.height = pct + '%'; } } function addFocusXP(amount) { _focusXP += amount; let maxXP = _getFocusMaxXP(_focusLevel); while (_focusXP >= maxXP) { _focusXP -= maxXP; _focusLevel++; maxXP = _getFocusMaxXP(_focusLevel); } _saveFocusData(); _renderFocusLevelUI(); } // Extend finishPomodoroSession to award XP (function() { const _orig = window.finishPomodoroSession; window.finishPomodoroSession = function() { if (typeof _orig === 'function') _orig.call(this); addFocusXP(_FOCUS_XP_PER_SESSION); }; })(); // Init: preload bg images immediately, load active companion, then preload all frames _preloadBgImages(); _switchCompanion(_activeCompanionIdx).then(_preloadAllInBackground); // Initial XP render _renderFocusLevelUI(); // ========================================== // PINCH-TO-SCALE — focus world companion widget // ========================================== (function() { const wrapper = document.getElementById('focusWorldWrapper'); if (!wrapper) return; const SCALE_MIN = 0.4; const SCALE_MAX = 1.2; const SCALE_KEY = 'fliqlow_fw_scale'; let _scale = parseFloat(localStorage.getItem(SCALE_KEY)) || 1; _scale = Math.min(SCALE_MAX, Math.max(SCALE_MIN, _scale)); wrapper.style.setProperty('--fw-scale', _scale); let _startDist = 0; let _startScale = 1; function _dist(touches) { const dx = touches[0].clientX - touches[1].clientX; const dy = touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } wrapper.addEventListener('touchstart', function(e) { if (e.touches.length === 2) { _startDist = _dist(e.touches); _startScale = _scale; } }, { passive: true }); wrapper.addEventListener('touchmove', function(e) { if (e.touches.length === 2) { e.preventDefault(); const ratio = _dist(e.touches) / _startDist; _scale = Math.min(SCALE_MAX, Math.max(SCALE_MIN, _startScale * ratio)); wrapper.style.setProperty('--fw-scale', _scale); } }, { passive: false }); wrapper.addEventListener('touchend', function(e) { if (e.touches.length < 2) { localStorage.setItem(SCALE_KEY, String(_scale)); } }, { passive: true }); })(); // ========================================== // PAGE-NAV AUTO-HIDE ON INACTIVITY // ========================================== (function() { const nav = document.querySelector('.page-nav'); if (!nav) return; const HIDE_DELAY = 2800; let _hideTimer = null; function showNav() { nav.classList.remove('page-nav--hidden'); clearTimeout(_hideTimer); _hideTimer = setTimeout(function() { nav.classList.add('page-nav--hidden'); }, HIDE_DELAY); } // Any of these events resets the timer and shows the nav var _events = ['scroll', 'mousemove', 'touchstart', 'keydown']; _events.forEach(function(evt) { document.addEventListener(evt, showNav, { passive: true }); }); // Visible on page load, then auto-hides after HIDE_DELAY showNav(); })();